Specify backend servers' weight via annotation for kubernetes

This commit is contained in:
Kim Min 2018-07-01 17:26:03 +08:00 committed by Traefiker Bot
parent f9b1106df2
commit e8e36bd9d5
8 changed files with 1055 additions and 2 deletions

View file

@ -155,9 +155,11 @@ The following general annotations are applicable on the Ingress object:
| `traefik.ingress.kubernetes.io/redirect-replacement: http://mydomain/$1` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-regex`. |
| `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. |
| `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Override the default frontend rule type. Default: `PathPrefix`. |
| `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access. all source IPs are permitted if the list is empty or a single range is ill-formatted. Please note, you may have to set `service.spec.externalTrafficPolicy` to the value `Local` to preserve the source IP of the request for filtering. Please see [this link](https://kubernetes.io/docs/tutorials/services/source-ip/) for more information.|
| `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access (6). |
| `ingress.kubernetes.io/whitelist-x-forwarded-for: "true"` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
| `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) |
| `traefik.ingress.kubernetes.io/service-weights: <YML>` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5) |
<1> `traefik.ingress.kubernetes.io/error-pages` example:
@ -205,6 +207,39 @@ Non-root paths will not be affected by this annotation and handled normally.
This annotation may not be combined with the `ReplacePath` rule type or any other annotation leveraging that rule type.
Trying to do so leads to an error and the corresponding Ingress object being ignored.
<5> `traefik.ingress.kubernetes.io/service-weights`:
Service weights enable to split traffic across multiple backing services in a fine-grained manner.
A canonical use case are canary releases where a new deployment starts to receive a small percentage of traffic (e.g., 1%) and steadily increases over time as confidence in the new deployment improves.
Example:
```yaml
service_backend1: 12.50%
service_backend2: 12.50%
service_backend3: 75 # Same as 75%, the percentage sign is optional
```
A single service backend definition may be omitted; in this case, Traefik auto-completes that service backend to 100% automatically.
Conveniently, users need not bother to compute the percentage remainder for a main service backend.
For instance, in the example above `service_backend3` does not need to be specified to be assigned 75%.
!!! note
For each service weight given, the Ingress specification must include a backend item with the corresponding `serviceName` and (if given) matching path.
Currently, 3 decimal places for the weight are supported.
An attempt to exceed the precision should be avoided as it may lead to percentage computation flaws and, in consequence, Ingress parsing errors.
For each path definition, this annotation will fail if:
- the sum of backend weights exceeds 100% or
- the sum of backend weights is less than 100% without one or more omitted backends
<6> `traefik.ingress.kubernetes.io/whitelist-source-range`:
All source IPs are permitted if the list is empty or a single range is ill-formatted.
Please note, you may have to set `service.spec.externalTrafficPolicy` to the value `Local` to preserve the source IP of the request for filtering.
Please see [this link](https://kubernetes.io/docs/tutorials/services/source-ip/) for more information.
!!! note
Please note that `traefik.ingress.kubernetes.io/redirect-regex` and `traefik.ingress.kubernetes.io/redirect-replacement` do not have to be set if `traefik.ingress.kubernetes.io/redirect-entry-point` is defined for the redirection (they will not be used in this case).

View file

@ -31,6 +31,7 @@ const (
annotationKubernetesErrorPages = "ingress.kubernetes.io/error-pages"
annotationKubernetesBuffering = "ingress.kubernetes.io/buffering"
annotationKubernetesAppRoot = "ingress.kubernetes.io/app-root"
annotationKubernetesServiceWeights = "ingress.kubernetes.io/service-weights"
annotationKubernetesSSLForceHost = "ingress.kubernetes.io/ssl-force-host"
annotationKubernetesSSLRedirect = "ingress.kubernetes.io/ssl-redirect"

View file

@ -184,6 +184,18 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
}
templateObjects.TLS = append(templateObjects.TLS, tlsSection...)
var weightAllocator weightAllocator = &defaultWeightAllocator{}
annotationPercentageWeights := getAnnotationName(i.Annotations, annotationKubernetesServiceWeights)
if _, ok := i.Annotations[annotationPercentageWeights]; ok {
fractionalAllocator, err := newFractionalWeightAllocator(i, k8sClient)
if err != nil {
log.Errorf("failed to create fractional weight allocator for ingress %s/%s: %v", i.Namespace, i.Name, err)
continue
}
log.Debugf("Created custom weight allocator for %s/%s: %s", i.Namespace, i.Name, fractionalAllocator)
weightAllocator = fractionalAllocator
}
for _, r := range i.Spec.Rules {
if r.HTTP == nil {
log.Warn("Error in ingress: HTTP is nil")
@ -274,6 +286,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
templateObjects.Backends[baseName].Buffering = getBuffering(service)
protocol := label.DefaultProtocol
for _, port := range service.Spec.Ports {
if equalPorts(port, pa.Backend.ServicePort) {
if port.Port == 443 || strings.HasPrefix(port.Name, "https") {
@ -319,9 +332,10 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
if address.TargetRef != nil && address.TargetRef.Name != "" {
name = address.TargetRef.Name
}
templateObjects.Backends[baseName].Servers[name] = types.Server{
URL: url,
Weight: label.DefaultWeight,
Weight: weightAllocator.getWeight(r.Host, pa.Path, pa.Backend.ServiceName),
}
}
}

View file

@ -2269,7 +2269,122 @@ func TestProviderUpdateIngressStatus(t *testing.T) {
}
})
}
}
func TestPercentageWeightServiceAnnotation(t *testing.T) {
ingresses := []*extensionsv1beta1.Ingress{
buildIngress(
iAnnotation(annotationKubernetesServiceWeights, `
service1: 10%
`),
iNamespace("testing"),
iRules(
iRule(
iHost("host1"),
iPaths(
onePath(iPath("/foo"), iBackend("service1", intstr.FromString("8080"))),
onePath(iPath("/foo"), iBackend("service2", intstr.FromString("7070"))),
onePath(iPath("/bar"), iBackend("service2", intstr.FromString("7070"))),
)),
),
),
}
services := []*corev1.Service{
buildService(
sName("service1"),
sNamespace("testing"),
sUID("1"),
sSpec(
clusterIP("10.0.0.1"),
sPorts(sPort(8080, "")),
),
),
buildService(
sName("service2"),
sNamespace("testing"),
sUID("1"),
sSpec(
clusterIP("10.0.0.1"),
sPorts(sPort(7070, "")),
),
),
}
endpoints := []*corev1.Endpoints{
buildEndpoint(
eNamespace("testing"),
eName("service1"),
eUID("1"),
subset(
eAddresses(
eAddress("10.10.0.1"),
eAddress("10.10.0.2"),
),
ePorts(ePort(8080, "")),
),
),
buildEndpoint(
eNamespace("testing"),
eName("service2"),
eUID("1"),
subset(
eAddresses(
eAddress("10.10.0.3"),
eAddress("10.10.0.4"),
),
ePorts(ePort(7070, "")),
),
),
}
watchChan := make(chan interface{})
client := clientMock{
ingresses: ingresses,
services: services,
endpoints: endpoints,
watchChan: watchChan,
}
provider := Provider{}
actual, err := provider.loadIngresses(client)
require.NoError(t, err, "error loading ingresses")
expected := buildConfiguration(
backends(
backend("host1/foo",
servers(
server("http://10.10.0.1:8080", weight(int(newPercentageValueFromFloat64(0.05)))),
server("http://10.10.0.2:8080", weight(int(newPercentageValueFromFloat64(0.05)))),
server("http://10.10.0.3:7070", weight(int(newPercentageValueFromFloat64(0.45)))),
server("http://10.10.0.4:7070", weight(int(newPercentageValueFromFloat64(0.45)))),
),
lbMethod("wrr"),
),
backend("host1/bar",
servers(
server("http://10.10.0.3:7070", weight(int(newPercentageValueFromFloat64(0.5)))),
server("http://10.10.0.4:7070", weight(int(newPercentageValueFromFloat64(0.5)))),
),
lbMethod("wrr"),
),
),
frontends(
frontend("host1/bar",
passHostHeader(),
routes(
route("/bar", "PathPrefix:/bar"),
route("host1", "Host:host1")),
),
frontend("host1/foo",
passHostHeader(),
routes(
route("/foo", "PathPrefix:/foo"),
route("host1", "Host:host1")),
),
),
)
assert.Equal(t, expected, actual, "error loading percentage weight annotation")
}
func TestProviderNewK8sInClusterClient(t *testing.T) {

View file

@ -0,0 +1,47 @@
package kubernetes
import (
"strconv"
"strings"
)
const defaultPercentageValuePrecision = 3
// percentageValue is int64 form of percentage value with 10^-3 precision.
type percentageValue int64
// toFloat64 returns its decimal float64 value.
func (v percentageValue) toFloat64() float64 {
return float64(v) / (1000 * 100)
}
func (v percentageValue) computeWeight(count int) int {
if count == 0 {
return 0
}
return int(float64(v) / float64(count))
}
// String returns its string form of percentage value.
func (v percentageValue) String() string {
return strconv.FormatFloat(v.toFloat64()*100, 'f', defaultPercentageValuePrecision, 64) + "%"
}
// newPercentageValueFromString tries to read percentage value from string, it can be either "1.1" or "1.1%", "6%".
// It will lose the extra precision if there are more digits after decimal point.
func newPercentageValueFromString(rawValue string) (percentageValue, error) {
if strings.HasSuffix(rawValue, "%") {
rawValue = rawValue[:len(rawValue)-1]
}
value, err := strconv.ParseFloat(rawValue, 64)
if err != nil {
return 0, err
}
return newPercentageValueFromFloat64(value) / 100, nil
}
// newPercentageValueFromFloat64 reads percentage value from float64
func newPercentageValueFromFloat64(f float64) percentageValue {
return percentageValue(f * (1000 * 100))
}

View file

@ -0,0 +1,196 @@
package kubernetes
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewPercentageValueFromFloat64(t *testing.T) {
testCases := []struct {
desc string
value float64
expectedString string
expectedFloat64 float64
}{
{
value: 0.01,
expectedString: "1.000%",
expectedFloat64: 0.01,
},
{
value: 0.5,
expectedString: "50.000%",
expectedFloat64: 0.5,
},
{
value: 0.99,
expectedString: "99.000%",
expectedFloat64: 0.99,
},
{
value: 0.99999,
expectedString: "99.999%",
expectedFloat64: 0.99999,
},
{
value: -0.99999,
expectedString: "-99.999%",
expectedFloat64: -0.99999,
},
{
value: -0.9999999,
expectedString: "-99.999%",
expectedFloat64: -0.99999,
},
{
value: 0,
expectedString: "0.000%",
expectedFloat64: 0,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
pvFromFloat64 := newPercentageValueFromFloat64(test.value)
assert.Equal(t, test.expectedString, pvFromFloat64.String(), "percentage string value mismatched")
assert.Equal(t, test.expectedFloat64, pvFromFloat64.toFloat64(), "percentage float64 value mismatched")
})
}
}
func TestNewPercentageValueFromString(t *testing.T) {
testCases := []struct {
desc string
value string
expectError bool
expectedString string
expectedFloat64 float64
}{
{
value: "1%",
expectError: false,
expectedString: "1.000%",
expectedFloat64: 0.01,
},
{
value: "0.5",
expectError: false,
expectedString: "0.500%",
expectedFloat64: 0.005,
},
{
value: "99%",
expectError: false,
expectedString: "99.000%",
expectedFloat64: 0.99,
},
{
value: "99.9%",
expectError: false,
expectedString: "99.900%",
expectedFloat64: 0.999,
},
{
value: "-99.9%",
expectError: false,
expectedString: "-99.900%",
expectedFloat64: -0.999,
},
{
value: "-99.99999%",
expectError: false,
expectedString: "-99.999%",
expectedFloat64: -0.99999,
},
{
value: "0%",
expectError: false,
expectedString: "0.000%",
expectedFloat64: 0,
},
{
value: "%",
expectError: true,
},
{
value: "foo",
expectError: true,
},
{
value: "",
expectError: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
pvFromString, err := newPercentageValueFromString(test.value)
if test.expectError {
require.Error(t, err, "expecting error but not happening")
} else {
require.NoError(t, err, "fail to parse percentage value")
assert.Equal(t, test.expectedString, pvFromString.String(), "percentage string value mismatched")
assert.Equal(t, test.expectedFloat64, pvFromString.toFloat64(), "percentage float64 value mismatched")
}
})
}
}
func TestNewPercentageValue(t *testing.T) {
testCases := []struct {
desc string
stringValue string
floatValue float64
}{
{
desc: "percentage",
stringValue: "1%",
floatValue: 0.01,
},
{
desc: "decimal",
stringValue: "0.5",
floatValue: 0.005,
},
{
desc: "negative percentage",
stringValue: "-99.999%",
floatValue: -0.99999,
},
{
desc: "negative decimal",
stringValue: "-0.99999",
floatValue: -0.0099999,
},
{
desc: "zero",
stringValue: "0%",
floatValue: 0,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
pvFromString, err := newPercentageValueFromString(test.stringValue)
require.NoError(t, err, "fail to parse percentage value")
pvFromFloat64 := newPercentageValueFromFloat64(test.floatValue)
assert.Equal(t, pvFromString, pvFromFloat64)
})
}
}

View file

@ -0,0 +1,190 @@
package kubernetes
import (
"fmt"
"sort"
"strings"
"github.com/containous/traefik/provider/label"
"gopkg.in/yaml.v2"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
)
type weightAllocator interface {
getWeight(host, path, serviceName string) int
}
var _ weightAllocator = &defaultWeightAllocator{}
var _ weightAllocator = &fractionalWeightAllocator{}
type defaultWeightAllocator struct{}
func (d *defaultWeightAllocator) getWeight(host, path, serviceName string) int {
return label.DefaultWeight
}
type ingressService struct {
host string
path string
service string
}
type fractionalWeightAllocator map[ingressService]int
// String returns a string representation as service name / percentage tuples
// sorted by service names.
// Example: [foo-svc: 30.000% bar-svc: 70.000%]
func (f *fractionalWeightAllocator) String() string {
var sorted []ingressService
for ingServ := range map[ingressService]int(*f) {
sorted = append(sorted, ingServ)
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].service < sorted[j].service
})
var res []string
for _, ingServ := range sorted {
res = append(res, fmt.Sprintf("%s: %s", ingServ.service, percentageValue(map[ingressService]int(*f)[ingServ])))
}
return fmt.Sprintf("[%s]", strings.Join(res, " "))
}
func newFractionalWeightAllocator(ingress *extensionsv1beta1.Ingress, client Client) (*fractionalWeightAllocator, error) {
servicePercentageWeights, err := getServicesPercentageWeights(ingress)
if err != nil {
return nil, err
}
serviceInstanceCounts, err := getServiceInstanceCounts(ingress, client)
if err != nil {
return nil, err
}
serviceWeights := map[ingressService]int{}
for _, rule := range ingress.Spec.Rules {
// key: rule path string
// value: service names
fractionalPathServices := map[string][]string{}
// key: rule path string
// value: fractional percentage weight
fractionalPathWeights := map[string]percentageValue{}
for _, pa := range rule.HTTP.Paths {
if _, ok := fractionalPathWeights[pa.Path]; !ok {
fractionalPathWeights[pa.Path] = newPercentageValueFromFloat64(1)
}
if weight, ok := servicePercentageWeights[pa.Backend.ServiceName]; ok {
ingSvc := ingressService{
host: rule.Host,
path: pa.Path,
service: pa.Backend.ServiceName,
}
serviceWeights[ingSvc] = weight.computeWeight(serviceInstanceCounts[ingSvc])
fractionalPathWeights[pa.Path] -= weight
if fractionalPathWeights[pa.Path].toFloat64() < 0 {
assignedWeight := newPercentageValueFromFloat64(1) - fractionalPathWeights[pa.Path]
return nil, fmt.Errorf("percentage value %s must not exceed 100%%", assignedWeight.String())
}
} else {
fractionalPathServices[pa.Path] = append(fractionalPathServices[pa.Path], pa.Backend.ServiceName)
}
}
for pa, fractionalWeight := range fractionalPathWeights {
fractionalServices := fractionalPathServices[pa]
if len(fractionalServices) == 0 {
if fractionalWeight > 0 {
assignedWeight := newPercentageValueFromFloat64(1) - fractionalWeight
return nil, fmt.Errorf("the sum of weights(%s) in the path %s%s must be 100%% when no omitted fractional service left", assignedWeight.String(), rule.Host, pa)
}
continue
}
totalFractionalInstanceCount := 0
for _, svc := range fractionalServices {
totalFractionalInstanceCount += serviceInstanceCounts[ingressService{
host: rule.Host,
path: pa,
service: svc,
}]
}
for _, svc := range fractionalServices {
ingSvc := ingressService{
host: rule.Host,
path: pa,
service: svc,
}
serviceWeights[ingSvc] = fractionalWeight.computeWeight(totalFractionalInstanceCount)
}
}
}
allocator := fractionalWeightAllocator(serviceWeights)
return &allocator, nil
}
func (f *fractionalWeightAllocator) getWeight(host, path, serviceName string) int {
return map[ingressService]int(*f)[ingressService{
host: host,
path: path,
service: serviceName,
}]
}
func getServicesPercentageWeights(ingress *extensionsv1beta1.Ingress) (map[string]percentageValue, error) {
percentageWeight := make(map[string]string)
annotationPercentageWeights := getAnnotationName(ingress.Annotations, annotationKubernetesServiceWeights)
if err := yaml.Unmarshal([]byte(ingress.Annotations[annotationPercentageWeights]), percentageWeight); err != nil {
return nil, err
}
servicesPercentageWeights := make(map[string]percentageValue)
for serviceName, percentageStr := range percentageWeight {
percentageValue, err := newPercentageValueFromString(percentageStr)
if err != nil {
return nil, fmt.Errorf("invalid percentage value %q", percentageStr)
}
servicesPercentageWeights[serviceName] = percentageValue
}
return servicesPercentageWeights, nil
}
func getServiceInstanceCounts(ingress *extensionsv1beta1.Ingress, client Client) (map[ingressService]int, error) {
serviceInstanceCounts := map[ingressService]int{}
for _, rule := range ingress.Spec.Rules {
for _, pa := range rule.HTTP.Paths {
count := 0
endpoints, exists, err := client.GetEndpoints(ingress.Namespace, pa.Backend.ServiceName)
if err != nil {
return nil, fmt.Errorf("failed to get endpoints %s/%s: %v", ingress.Namespace, pa.Backend.ServiceName, err)
}
if !exists {
return nil, fmt.Errorf("endpoints not found for %s/%s", ingress.Namespace, pa.Backend.ServiceName)
}
for _, subset := range endpoints.Subsets {
count += len(subset.Addresses)
}
serviceInstanceCounts[ingressService{
host: rule.Host,
path: pa.Path,
service: pa.Backend.ServiceName,
}] += count
}
}
return serviceInstanceCounts, nil
}

View file

@ -0,0 +1,455 @@
package kubernetes
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
func TestString(t *testing.T) {
pv1 := newPercentageValueFromFloat64(0.5)
pv2 := newPercentageValueFromFloat64(0.2)
pv3 := newPercentageValueFromFloat64(0.3)
f := fractionalWeightAllocator(
map[ingressService]int{
{
host: "host2",
path: "path2",
service: "service2",
}: int(pv2),
{
host: "host3",
path: "path3",
service: "service3",
}: int(pv3),
{
host: "host1",
path: "path1",
service: "service1",
}: int(pv1),
},
)
expected := fmt.Sprintf("[service1: %s service2: %s service3: %s]", pv1, pv2, pv3)
actual := f.String()
assert.Equal(t, expected, actual)
}
func TestGetServicesPercentageWeights(t *testing.T) {
testCases := []struct {
desc string
annotationValue string
expectError bool
expectedWeights map[string]percentageValue
}{
{
desc: "empty annotation",
annotationValue: ``,
expectedWeights: map[string]percentageValue{},
},
{
desc: "50% fraction",
annotationValue: `
service1: 10%
service2: 20%
service3: 20%
`,
expectedWeights: map[string]percentageValue{
"service1": newPercentageValueFromFloat64(0.1),
"service2": newPercentageValueFromFloat64(0.2),
"service3": newPercentageValueFromFloat64(0.2),
},
},
{
desc: "50% fraction with empty fraction",
annotationValue: `
service1: 10%
service2: 20%
service3: 20%
service4:
`,
expectError: true,
},
{
desc: "50% fraction float form",
annotationValue: `
service1: 0.1
service2: 0.2
service3: 0.2
`,
expectedWeights: map[string]percentageValue{
"service1": newPercentageValueFromFloat64(0.001),
"service2": newPercentageValueFromFloat64(0.002),
"service3": newPercentageValueFromFloat64(0.002),
},
},
{
desc: "no fraction",
annotationValue: `
service1: 10%
service2: 90%
`,
expectedWeights: map[string]percentageValue{
"service1": newPercentageValueFromFloat64(0.1),
"service2": newPercentageValueFromFloat64(0.9),
},
},
{
desc: "extra weight specification",
annotationValue: `
service1: 90%
service5: 90%
`,
expectedWeights: map[string]percentageValue{
"service1": newPercentageValueFromFloat64(0.9),
"service5": newPercentageValueFromFloat64(0.9),
},
},
{
desc: "malformed annotation",
annotationValue: `
service1- 90%
service5- 90%
`,
expectError: true,
expectedWeights: nil,
},
{
desc: "more than one hundred percentaged service",
annotationValue: `
service1: 100%
service2: 1%
`,
expectedWeights: map[string]percentageValue{
"service1": newPercentageValueFromFloat64(1),
"service2": newPercentageValueFromFloat64(0.01),
},
},
{
desc: "incorrect percentage value",
annotationValue: `
service1: 1000%
`,
expectedWeights: map[string]percentageValue{
"service1": newPercentageValueFromFloat64(10),
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
ingress := &extensionsv1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Annotations: map[string]string{
annotationKubernetesServiceWeights: test.annotationValue,
},
},
}
weights, err := getServicesPercentageWeights(ingress)
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expectedWeights, weights)
}
})
}
}
func TestComputeServiceWeights(t *testing.T) {
client := clientMock{
endpoints: []*corev1.Endpoints{
buildEndpoint(
eNamespace("testing"),
eName("service1"),
eUID("1"),
subset(
eAddresses(eAddress("10.10.0.1")),
ePorts(ePort(8080, ""))),
subset(
eAddresses(eAddress("10.21.0.2")),
ePorts(ePort(8080, ""))),
),
buildEndpoint(
eNamespace("testing"),
eName("service2"),
eUID("2"),
subset(
eAddresses(eAddress("10.10.0.3")),
ePorts(ePort(8080, ""))),
),
buildEndpoint(
eNamespace("testing"),
eName("service3"),
eUID("3"),
subset(
eAddresses(eAddress("10.10.0.4")),
ePorts(ePort(8080, ""))),
subset(
eAddresses(eAddress("10.21.0.5")),
ePorts(ePort(8080, ""))),
subset(
eAddresses(eAddress("10.21.0.6")),
ePorts(ePort(8080, ""))),
subset(
eAddresses(eAddress("10.21.0.7")),
ePorts(ePort(8080, ""))),
),
buildEndpoint(
eNamespace("testing"),
eName("service4"),
eUID("4"),
subset(
eAddresses(eAddress("10.10.0.7")),
ePorts(ePort(8080, ""))),
),
},
}
testCases := []struct {
desc string
ingress *extensionsv1beta1.Ingress
expectError bool
expectedWeights map[ingressService]percentageValue
}{
{
desc: "1 path 2 service",
ingress: buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesServiceWeights, `
service1: 10%
`),
iRules(
iRule(iHost("foo.test"), iPaths(
onePath(iPath("/foo"), iBackend("service1", intstr.FromInt(8080))),
onePath(iPath("/foo"), iBackend("service2", intstr.FromInt(8080))),
)),
),
),
expectError: false,
expectedWeights: map[ingressService]percentageValue{
{
host: "foo.test",
path: "/foo",
service: "service1",
}: newPercentageValueFromFloat64(0.05),
{
host: "foo.test",
path: "/foo",
service: "service2",
}: newPercentageValueFromFloat64(0.90),
},
},
{
desc: "2 path 2 service",
ingress: buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesServiceWeights, `
service1: 60%
`),
iRules(
iRule(iHost("foo.test"), iPaths(
onePath(iPath("/foo"), iBackend("service1", intstr.FromInt(8080))),
onePath(iPath("/foo"), iBackend("service2", intstr.FromInt(8080))),
onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(8080))),
onePath(iPath("/bar"), iBackend("service3", intstr.FromInt(8080))),
)),
),
),
expectError: false,
expectedWeights: map[ingressService]percentageValue{
{
host: "foo.test",
path: "/foo",
service: "service1",
}: newPercentageValueFromFloat64(0.30),
{
host: "foo.test",
path: "/foo",
service: "service2",
}: newPercentageValueFromFloat64(0.40),
{
host: "foo.test",
path: "/bar",
service: "service1",
}: newPercentageValueFromFloat64(0.30),
{
host: "foo.test",
path: "/bar",
service: "service3",
}: newPercentageValueFromFloat64(0.10),
},
},
{
desc: "2 path 3 service",
ingress: buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesServiceWeights, `
service1: 20%
service3: 20%
`),
iRules(
iRule(iHost("foo.test"), iPaths(
onePath(iPath("/foo"), iBackend("service1", intstr.FromInt(8080))),
onePath(iPath("/foo"), iBackend("service2", intstr.FromInt(8080))),
onePath(iPath("/bar"), iBackend("service2", intstr.FromInt(8080))),
onePath(iPath("/bar"), iBackend("service3", intstr.FromInt(8080))),
)),
),
),
expectError: false,
expectedWeights: map[ingressService]percentageValue{
{
host: "foo.test",
path: "/foo",
service: "service1",
}: newPercentageValueFromFloat64(0.10),
{
host: "foo.test",
path: "/foo",
service: "service2",
}: newPercentageValueFromFloat64(0.80),
{
host: "foo.test",
path: "/bar",
service: "service3",
}: newPercentageValueFromFloat64(0.05),
{
host: "foo.test",
path: "/bar",
service: "service2",
}: newPercentageValueFromFloat64(0.80),
},
},
{
desc: "1 path 4 service",
ingress: buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesServiceWeights, `
service1: 20%
service2: 40%
service3: 40%
`),
iRules(
iRule(iHost("foo.test"), iPaths(
onePath(iPath("/foo"), iBackend("service1", intstr.FromInt(8080))),
onePath(iPath("/foo"), iBackend("service2", intstr.FromInt(8080))),
onePath(iPath("/foo"), iBackend("service3", intstr.FromInt(8080))),
onePath(iPath("/foo"), iBackend("service4", intstr.FromInt(8080))),
)),
),
),
expectError: false,
expectedWeights: map[ingressService]percentageValue{
{
host: "foo.test",
path: "/foo",
service: "service1",
}: newPercentageValueFromFloat64(0.10),
{
host: "foo.test",
path: "/foo",
service: "service2",
}: newPercentageValueFromFloat64(0.40),
{
host: "foo.test",
path: "/foo",
service: "service3",
}: newPercentageValueFromFloat64(0.10),
{
host: "foo.test",
path: "/foo",
service: "service4",
}: newPercentageValueFromFloat64(0.00),
},
},
{
desc: "2 path no service",
ingress: buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesServiceWeights, `
service1: 20%
service2: 40%
service3: 40%
`),
iRules(
iRule(iHost("foo.test"), iPaths(
onePath(iPath("/foo"), iBackend("noservice", intstr.FromInt(8080))),
onePath(iPath("/bar"), iBackend("noservice", intstr.FromInt(8080))),
)),
),
),
expectError: true,
},
{
desc: "2 path without weight",
ingress: buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesServiceWeights, ``),
iRules(
iRule(iHost("foo.test"), iPaths(
onePath(iPath("/foo"), iBackend("service1", intstr.FromInt(8080))),
onePath(iPath("/bar"), iBackend("service2", intstr.FromInt(8080))),
)),
),
),
expectError: false,
expectedWeights: map[ingressService]percentageValue{
{
host: "foo.test",
path: "/foo",
service: "service1",
}: newPercentageValueFromFloat64(0.50),
{
host: "foo.test",
path: "/bar",
service: "service2",
}: newPercentageValueFromFloat64(1.00),
},
},
{
desc: "2 path overflow",
ingress: buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesServiceWeights, `
service1: 70%
service2: 80%
`),
iRules(
iRule(iHost("foo.test"), iPaths(
onePath(iPath("/foo"), iBackend("service1", intstr.FromInt(8080))),
onePath(iPath("/foo"), iBackend("service2", intstr.FromInt(8080))),
)),
),
),
expectError: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
weightAllocator, err := newFractionalWeightAllocator(test.ingress, client)
if test.expectError {
require.Error(t, err)
} else {
for ingSvc, percentage := range test.expectedWeights {
assert.Equal(t, int(percentage), weightAllocator.getWeight(ingSvc.host, ingSvc.path, ingSvc.service))
}
}
})
}
}