1
0
Fork 0

feat: add highest random weight in kubernetes CRD

This commit is contained in:
Landry Benguigui 2025-09-19 10:18:04 +02:00 committed by GitHub
parent c09d3fb03c
commit 634f892370
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1328 additions and 24 deletions

View file

@ -0,0 +1,107 @@
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: whoami1-abc
namespace: default
labels:
kubernetes.io/service-name: whoami1
addressType: IPv4
ports:
- name: web
port: 8080
endpoints:
- addresses:
- 10.10.0.1
- 10.10.0.2
conditions:
ready: true
---
apiVersion: v1
kind: Service
metadata:
name: whoami1
namespace: default
spec:
ports:
- name: web
port: 8080
selector:
app: traefiklabs
task: whoami1
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: whoami2-abc
namespace: default
labels:
kubernetes.io/service-name: whoami2
addressType: IPv4
ports:
- name: web
port: 8080
endpoints:
- addresses:
- 10.10.0.3
- 10.10.0.4
conditions:
ready: true
---
apiVersion: v1
kind: Service
metadata:
name: whoami2
namespace: default
spec:
ports:
- name: web
port: 8080
selector:
app: traefiklabs
task: whoami2
---
apiVersion: traefik.io/v1alpha1
kind: TraefikService
metadata:
name: hrw1
namespace: default
spec:
highestRandomWeight:
services:
- name: whoami1
kind: Service
port: 8080
weight: 10
- name: whoami2
kind: Service
port: 8080
weight: 20
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`foo.com`) && PathPrefix(`/foo`)
kind: Rule
priority: 12
services:
- name: hrw1
kind: TraefikService

View file

@ -216,13 +216,17 @@ type configBuilder struct {
func (c configBuilder) buildTraefikService(ctx context.Context, tService *traefikv1alpha1.TraefikService, conf map[string]*dynamic.Service) error {
id := provider.Normalize(makeID(tService.Namespace, tService.Name))
if tService.Spec.Weighted != nil {
switch {
case tService.Spec.Weighted != nil:
return c.buildServicesLB(ctx, tService.Namespace, tService.Spec, id, conf)
} else if tService.Spec.Mirroring != nil {
case tService.Spec.Mirroring != nil:
return c.buildMirroring(ctx, tService, id, conf)
}
case tService.Spec.HighestRandomWeight != nil:
return c.buildHRW(ctx, tService, id, conf)
default:
return errors.New("unspecified service type")
return errors.New("unspecified service type")
}
}
// buildServicesLB creates the configuration for the load-balancer of services named id, and defined in tService.
@ -329,7 +333,7 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load
// TODO: remove this when the fake client apply default values.
if svc.Strategy != "" {
switch svc.Strategy {
case dynamic.BalancerStrategyWRR, dynamic.BalancerStrategyP2C:
case dynamic.BalancerStrategyWRR, dynamic.BalancerStrategyP2C, dynamic.BalancerStrategyHRW:
lb.Strategy = svc.Strategy
// Here we are just logging a warning as the default value is already applied.
@ -644,6 +648,38 @@ func (c configBuilder) nameAndService(ctx context.Context, parentNamespace strin
}
}
func (c configBuilder) buildHRW(ctx context.Context, tService *traefikv1alpha1.TraefikService, id string, conf map[string]*dynamic.Service) error {
var hrwServices []dynamic.HRWService
for _, hrwService := range tService.Spec.HighestRandomWeight.Services {
hrwServiceName, k8sService, err := c.nameAndService(ctx, tService.Namespace, hrwService.LoadBalancerSpec)
if err != nil {
return err
}
if k8sService != nil {
conf[hrwServiceName] = k8sService
}
weight := hrwService.Weight
if weight == nil {
weight = func(i int) *int { return &i }(1)
}
hrwServices = append(hrwServices, dynamic.HRWService{
Name: hrwServiceName,
Weight: weight,
})
}
conf[id] = &dynamic.Service{
HighestRandomWeight: &dynamic.HighestRandomWeight{
Services: hrwServices,
},
}
return nil
}
func splitSvcNameProvider(name string) (string, string) {
parts := strings.Split(name, providerNamespaceSeparator)

View file

@ -3150,6 +3150,79 @@ func TestLoadIngressRoutes(t *testing.T) {
},
},
},
{
desc: "one kube services in a highest random weight",
paths: []string{"with_highest_random_weight.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TLS: &dynamic.TLSConfiguration{},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-test-route-77c62dfe9517144aeeaa": {
EntryPoints: []string{"web"},
Service: "default-hrw1",
Rule: "Host(`foo.com`) && PathPrefix(`/foo`)",
Priority: 12,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-hrw1": {
HighestRandomWeight: &dynamic.HighestRandomWeight{
Services: []dynamic.HRWService{
{Name: "default-whoami1-8080", Weight: pointer(10)},
{Name: "default-whoami2-8080", Weight: pointer(20)},
},
},
},
"default-whoami1-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:8080",
},
{
URL: "http://10.10.0.2:8080",
},
},
PassHostHeader: pointer(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"default-whoami2-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "http://10.10.0.3:8080",
},
{
URL: "http://10.10.0.4:8080",
},
},
PassHostHeader: pointer(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{
desc: "One ingress Route with two different services, with weights",
paths: []string{"services.yml", "with_two_services_weight.yml"},

View file

@ -114,10 +114,10 @@ type LoadBalancerSpec struct {
// It defaults to https when Kubernetes Service port is 443, http otherwise.
Scheme string `json:"scheme,omitempty"`
// Strategy defines the load balancing strategy between the servers.
// Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices).
// Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
// RoundRobin value is deprecated and supported for backward compatibility.
// TODO: when the deprecated RoundRobin value will be removed, set the default value to wrr.
// +kubebuilder:validation:Enum=wrr;p2c;RoundRobin
// +kubebuilder:validation:Enum=wrr;p2c;hrw;RoundRobin
Strategy dynamic.BalancerStrategy `json:"strategy,omitempty"`
// PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service.
// By default, passHostHeader is true.

View file

@ -44,6 +44,8 @@ type TraefikServiceSpec struct {
Weighted *WeightedRoundRobin `json:"weighted,omitempty"`
// Mirroring defines the Mirroring service configuration.
Mirroring *Mirroring `json:"mirroring,omitempty"`
// HighestRandomWeight defines the highest random weight service configuration.
HighestRandomWeight *HighestRandomWeight `json:"highestRandomWeight,omitempty"`
}
// +k8s:deepcopy-gen=true
@ -86,3 +88,12 @@ type WeightedRoundRobin struct {
// More info: https://doc.traefik.io/traefik/v3.5/routing/providers/kubernetes-crd/#stickiness-and-load-balancing
Sticky *dynamic.Sticky `json:"sticky,omitempty"`
}
// +k8s:deepcopy-gen=true
// HighestRandomWeight holds the highest random weight configuration.
// More info: https://doc.traefik.io/traefik/v3.5/routing/services/#highest-random-configuration
type HighestRandomWeight struct {
// Services defines the list of Kubernetes Service and/or TraefikService to load-balance, with weight.
Services []Service `json:"services,omitempty"`
}

View file

@ -349,6 +349,29 @@ func (in *ForwardingTimeouts) DeepCopy() *ForwardingTimeouts {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HighestRandomWeight) DeepCopyInto(out *HighestRandomWeight) {
*out = *in
if in.Services != nil {
in, out := &in.Services, &out.Services
*out = make([]Service, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HighestRandomWeight.
func (in *HighestRandomWeight) DeepCopy() *HighestRandomWeight {
if in == nil {
return nil
}
out := new(HighestRandomWeight)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressRoute) DeepCopyInto(out *IngressRoute) {
*out = *in
@ -2028,6 +2051,11 @@ func (in *TraefikServiceSpec) DeepCopyInto(out *TraefikServiceSpec) {
*out = new(Mirroring)
(*in).DeepCopyInto(*out)
}
if in.HighestRandomWeight != nil {
in, out := &in.HighestRandomWeight, &out.HighestRandomWeight
*out = new(HighestRandomWeight)
(*in).DeepCopyInto(*out)
}
return
}