blob: 0b12d441a51c201fbd5c398e9d6d78aefa0e4bfc [file]
//go:build e2e
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package e2e
import (
"context"
"fmt"
"strings"
"testing"
"github.com/blang/semver/v4"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
annotationSourceCidrs = "service.beta.kubernetes.io/cloudstack-load-balancer-source-cidrs"
annotationHostname = "service.beta.kubernetes.io/cloudstack-load-balancer-hostname"
annotationIPAssociated = "service.beta.kubernetes.io/cloudstack-load-balancer-ip-associated-by-controller" //nolint:gosec
)
func TestAnnot_SourceCIDRs(t *testing.T) {
f := NewFramework(t)
svc := f.CreateLBService(func(s *corev1.Service) {
s.Annotations = map[string]string{
annotationSourceCidrs: "10.0.0.0/8,192.168.100.0/24",
}
})
lbName := defaultLoadBalancerName(svc)
f.WaitForIngressIP(svc)
rules := f.WaitForLBRules(lbName, 1)
for _, cidr := range []string{"10.0.0.0/8", "192.168.100.0/24"} {
if !strings.Contains(rules[0].Cidrlist, cidr) {
t.Errorf("rule cidrlist = %q, want it to contain %s", rules[0].Cidrlist, cidr)
}
}
originalRuleID := rules[0].Id
// Change the CIDR list. On >= 4.22 the rule is updated in place (same
// ID); on older versions it is deleted and recreated (new ID).
f.UpdateService(svc, func(s *corev1.Service) {
s.Annotations[annotationSourceCidrs] = "172.16.0.0/12"
})
// Poll only for propagation, then assert once on the settled rule. Doing
// the assertion inside the poll would tie it to the wait: if the CIDR
// never propagated, Eventually would fail first and the in-place versus
// recreate check would never run.
var settledRuleID string
f.Eventually(lbSyncTimeout, lbSyncInterval, "cidr list update to propagate",
func() (bool, error) {
current, err := f.LBRules(lbName)
if err != nil {
return false, err
}
if len(current) != 1 {
return false, fmt.Errorf("saw %d rules, want 1", len(current))
}
if !strings.Contains(current[0].Cidrlist, "172.16.0.0/12") {
return false, fmt.Errorf("cidrlist is %q, want it to contain 172.16.0.0/12",
current[0].Cidrlist)
}
settledRuleID = current[0].Id
return true, nil
})
// >= 4.22 updates the rule in place; older releases delete and recreate it.
inPlace := f.Version.GTE(semver.Version{Major: 4, Minor: 22, Patch: 0})
if inPlace && settledRuleID != originalRuleID {
t.Errorf("expected in-place cidr update on %s (rule ID changed %s -> %s)",
f.Version, originalRuleID, settledRuleID)
}
if !inPlace && settledRuleID == originalRuleID {
t.Errorf("expected rule recreation on %s (rule ID unchanged)", f.Version)
}
}
func TestAnnot_Hostname(t *testing.T) {
f := NewFramework(t)
svc := f.CreateLBService(func(s *corev1.Service) {
s.Annotations = map[string]string{
annotationHostname: "lb.example.com",
}
})
ingress := f.WaitForIngressIP(svc)
if ingress.Hostname != "lb.example.com" {
t.Errorf("ingress hostname = %q, want lb.example.com", ingress.Hostname)
}
if ingress.IP != "" {
t.Errorf("ingress IP = %q, want empty when hostname annotation is set", ingress.IP)
}
}
func TestAnnot_SessionAffinity(t *testing.T) {
f := NewFramework(t)
svc := f.CreateLBService(func(s *corev1.Service) {
s.Spec.SessionAffinity = corev1.ServiceAffinityClientIP
})
lbName := defaultLoadBalancerName(svc)
f.WaitForIngressIP(svc)
rules := f.WaitForLBRules(lbName, 1)
if rules[0].Algorithm != "source" {
t.Errorf("algorithm = %q, want source for sessionAffinity ClientIP", rules[0].Algorithm)
}
f.UpdateService(svc, func(s *corev1.Service) {
s.Spec.SessionAffinity = corev1.ServiceAffinityNone
})
f.Eventually(lbSyncTimeout, lbSyncInterval, "algorithm to revert to roundrobin",
func() (bool, error) {
current, err := f.LBRules(lbName)
if err != nil || len(current) != 1 {
return false, err
}
return current[0].Algorithm == "roundrobin", nil
})
}
func TestAnnot_ExplicitLoadBalancerIP(t *testing.T) {
f := NewFramework(t)
// Pick a free IP from the simulator's public range instead of hardcoding
// one that a parallel test may have grabbed.
freeIP, err := f.FreePublicIP()
if err != nil {
t.Fatalf("finding a free public IP: %v", err)
}
svc := f.CreateLBService(func(s *corev1.Service) {
s.Spec.LoadBalancerIP = freeIP
})
ingress := f.WaitForIngressIP(svc)
if ingress.IP != freeIP {
t.Fatalf("ingress IP = %q, want requested %q", ingress.IP, freeIP)
}
// The controller associated the IP itself, so it must record that fact
// on the service; on deletion the IP must be released again.
f.Eventually(lbSyncTimeout, lbSyncInterval, "ip-associated-by-controller annotation",
func() (bool, error) {
current, err := f.K8s.CoreV1().Services(svc.Namespace).Get(
context.Background(), svc.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
return current.Annotations[annotationIPAssociated] == "true", nil
})
f.DeleteServiceAndWait(svc)
f.Eventually(lbSyncTimeout, lbSyncInterval, "explicitly requested IP to be released",
func() (bool, error) {
ip, err := f.PublicIPByAddress(freeIP)
if err != nil || ip == nil {
return false, err
}
return ip.Allocated == "", nil
})
}