blob: 55392178b1d6c553dfafe3e7240a3c139765f788 [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"
"os"
"strconv"
"strings"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// vpcFramework skips unless the harness is in the VPC phase.
//
// 50-topology-vpc.sh appends E2E_ACL_ID, E2E_VPC_ID and E2E_PROJECT_ID to
// hack/e2e/_out/ids.env. Note that the project is read from CS_PROJECT_ID, not
// E2E_PROJECT_ID, because it also configures the CloudStack client in
// NewFramework; the test runner maps one to the other. To run this phase by
// hand:
//
// . hack/e2e/_out/ids.env
// CS_PROJECT_ID="$E2E_PROJECT_ID" go test -tags e2e ./test/e2e/... -run TestVPC
func vpcFramework(t *testing.T) (*Framework, string, string) {
t.Helper()
aclID := os.Getenv("E2E_ACL_ID")
vpcID := os.Getenv("E2E_VPC_ID")
if aclID == "" || vpcID == "" || os.Getenv("CS_PROJECT_ID") == "" {
t.Skip("E2E_ACL_ID/E2E_VPC_ID/CS_PROJECT_ID not set; skipping VPC phase test " +
"(CS_PROJECT_ID is set from E2E_PROJECT_ID in ids.env)")
}
return NewFramework(t), aclID, vpcID
}
// TestVPC_LoadBalancer covers the VPC path end to end: the LB rule is
// created, the public IP is associated with the VPC, ingress traffic is
// allowed via a Network ACL rule on the custom ACL list (not a firewall
// rule), and everything is cleaned up on delete.
func TestVPC_LoadBalancer(t *testing.T) {
f, aclID, vpcID := vpcFramework(t)
svc := f.CreateLBService(nil)
lbName := defaultLoadBalancerName(svc)
f.WaitForIngressIP(svc)
rules := f.WaitForLBRules(lbName, 1)
rule := rules[0]
ip, err := f.PublicIP(rule.Publicipid)
if err != nil || ip == nil {
t.Fatalf("fetching public IP %s: %v", rule.Publicipid, err)
}
if ip.Vpcid != vpcID {
t.Errorf("public IP vpcid = %q, want %q", ip.Vpcid, vpcID)
}
f.Eventually(lbSyncTimeout, lbSyncInterval, "network ACL rule for port 80",
func() (bool, error) {
aclRules, err := f.ACLRules(aclID)
if err != nil {
return false, err
}
for _, r := range aclRules {
if r.Startport == "80" && r.Endport == "80" &&
strings.EqualFold(r.Protocol, "tcp") &&
strings.EqualFold(r.Action, "Allow") &&
strings.EqualFold(r.Traffictype, "Ingress") {
return true, nil
}
}
return false, nil
})
// The tier offering has no Firewall service, so no firewall rule belongs here.
fwRules, err := f.FirewallRules(rule.Publicipid)
if err != nil {
t.Fatalf("listing firewall rules: %v", err)
}
for _, fw := range fwRules {
if fw.Startport == 80 && fw.Endport == 80 {
t.Errorf("unexpected firewall rule on VPC public IP: %+v", fw)
}
}
f.DeleteServiceAndWait(svc)
f.Eventually(lbSyncTimeout, lbSyncInterval, "network ACL rule to be removed",
func() (bool, error) {
aclRules, err := f.ACLRules(aclID)
if err != nil {
return false, err
}
for _, r := range aclRules {
if r.Startport == "80" && r.Endport == "80" && strings.EqualFold(r.Protocol, "tcp") {
return false, nil
}
}
return true, nil
})
}
// TestVPC_NodesReinitialized asserts the CCM re-initialized the nodes against
// the project VMs after the phase switch.
func TestVPC_NodesReinitialized(t *testing.T) {
f, _, _ := vpcFramework(t)
for _, node := range f.Nodes() {
vm, err := f.VMByName(node.Name)
if err != nil {
t.Fatalf("looking up project VM for node %s: %v", node.Name, err)
}
if vm == nil {
t.Errorf("no project VM named %s visible with CS_PROJECT_ID", node.Name)
}
}
for _, node := range f.Nodes() {
for _, taint := range node.Spec.Taints {
if taint.Key == "node.cloudprovider.kubernetes.io/uninitialized" {
t.Errorf("node %s still has the uninitialized taint", node.Name)
}
}
}
}
// countACLRules returns how many ingress ACL rules on the list target a port.
func countACLRules(f *Framework, aclID, port string) (int, error) {
rules, err := f.ACLRules(aclID)
if err != nil {
return 0, err
}
n := 0
for _, r := range rules {
if r.Startport == port && r.Endport == port && strings.EqualFold(r.Protocol, "tcp") {
n++
}
}
return n, nil
}
// TestVPC_ACLRuleNotDuplicatedOnResync is a regression test for a
// project-scoping bug in updateNetworkACL: it listed the existing ACL rules
// without the project, so with project-id set it never saw the rule it had
// just created and appended another one on every reconcile.
func TestVPC_ACLRuleNotDuplicatedOnResync(t *testing.T) {
f, aclID, _ := vpcFramework(t)
// An ACL rule belongs to the tier, not the service, so sharing port 80 with
// TestVPC_LoadBalancer would blur the count.
const port = "8080"
svc := f.CreateLBService(func(s *corev1.Service) {
s.Spec.Ports = []corev1.ServicePort{
{Name: "http", Port: 8080, Protocol: corev1.ProtocolTCP},
}
})
lbName := defaultLoadBalancerName(svc)
f.WaitForIngressIP(svc)
f.Eventually(lbSyncTimeout, lbSyncInterval, "the ACL rule for port "+port,
func() (bool, error) {
n, err := countACLRules(f, aclID, port)
return n >= 1, err
})
forceReconcile(f, svc, lbName)
n, err := countACLRules(f, aclID, port)
if err != nil {
t.Fatalf("counting ACL rules: %v", err)
}
if n != 1 {
t.Errorf("ACL rules for port %s = %d, want exactly 1; the reconcile duplicated the rule", port, n)
}
}
// forceReconcile makes the service controller run EnsureLoadBalancer again by
// flipping sessionAffinity, then waits for the resulting algorithm change so the
// caller observes a reconcile that has demonstrably completed.
func forceReconcile(f *Framework, svc *corev1.Service, lbName string) {
f.T.Helper()
f.UpdateService(svc, func(s *corev1.Service) {
s.Spec.SessionAffinity = corev1.ServiceAffinityClientIP
})
f.Eventually(lbSyncTimeout, lbSyncInterval, "the reconcile to apply the new algorithm",
func() (bool, error) {
rules, err := f.LBRules(lbName)
if err != nil || len(rules) != 1 {
return false, err
}
return rules[0].Algorithm == "source", nil
})
}
// TestVPC_ExplicitLoadBalancerIPReleased is a regression test for a
// project-scoping bug in EnsureLoadBalancerDeleted: the disassociation check
// looked the public IP up without the project, so with project-id set the
// lookup failed, the controller decided not to disassociate, and the IP leaked
// on every deletion.
//
// It only reproduces with spec.loadBalancerIP set: an auto-allocated IP is
// released unconditionally and never reaches that check.
func TestVPC_ExplicitLoadBalancerIPReleased(t *testing.T) {
f, _, _ := vpcFramework(t)
freeIP, err := f.FreePublicIP()
if err != nil {
t.Fatalf("finding a free public IP: %v", err)
}
// The VPC virtual router rejects most public ports with "LB service provider
// cannot support this rule"; 80 is one it accepts.
svc := f.CreateLBService(func(s *corev1.Service) {
s.Spec.LoadBalancerIP = freeIP
s.Spec.Ports = []corev1.ServicePort{
{Name: "http", Port: 80, Protocol: corev1.ProtocolTCP},
}
})
if ingress := f.WaitForIngressIP(svc); ingress.IP != freeIP {
t.Fatalf("ingress IP = %q, want requested %q", ingress.IP, freeIP)
}
// The annotation is what routes deletion through the disassociation path under test.
f.Eventually(lbSyncTimeout, lbSyncInterval, "the 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, "the 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
})
}
// TestVPC_ProxyProtocolACL covers the proxy protocol on a VPC tier, where
// ingress is opened with a Network ACL rule rather than a firewall rule.
// updateNetworkACL used to create the rule with the CloudStack protocol name
// tcp-proxy, which the API rejects, so a proxy protocol service on a tier never
// reconciled at all. The ACL rule is keyed on the IP protocol, so it must be
// created as tcp and be the same single rule before and after the toggle.
func TestVPC_ProxyProtocolACL(t *testing.T) {
f, aclID, _ := vpcFramework(t)
// An ACL rule belongs to the tier, so this test uses a port of its own. Note
// that 8081 is the virtual router's HAProxy stats port, which CloudStack
// refuses to load balance.
const servicePort int32 = 8085
port := strconv.Itoa(int(servicePort))
svc := f.CreateLBService(func(s *corev1.Service) {
s.Annotations = map[string]string{annotationProxyProtocol: "true"}
s.Spec.Ports = []corev1.ServicePort{
{Name: "http", Port: servicePort, Protocol: corev1.ProtocolTCP},
}
})
lbName := defaultLoadBalancerName(svc)
f.WaitForIngressIP(svc)
rules := f.WaitForLBRules(lbName, 1)
if rules[0].Protocol != "tcp-proxy" {
t.Errorf("rule protocol = %q, want tcp-proxy", rules[0].Protocol)
}
f.Eventually(lbSyncTimeout, lbSyncInterval, "the tcp network ACL rule for port "+port,
func() (bool, error) {
n, err := countACLRules(f, aclID, port)
return n >= 1, err
})
aclRules, err := f.ACLRules(aclID)
if err != nil {
t.Fatalf("listing ACL rules: %v", err)
}
for _, r := range aclRules {
if r.Startport == port && !strings.EqualFold(r.Protocol, "tcp") {
t.Errorf("ACL rule for port %s has protocol %q, want tcp", port, r.Protocol)
}
}
// Turning the annotation off keeps the one ACL rule: both protocols share it.
f.UpdateService(svc, func(s *corev1.Service) {
delete(s.Annotations, annotationProxyProtocol)
})
f.Eventually(lbSyncTimeout, lbSyncInterval, "the rule to settle back on tcp",
func() (bool, error) {
current, err := f.LBRules(lbName)
if err != nil || len(current) != 1 {
return false, err
}
return current[0].Protocol == "tcp", nil
})
n, err := countACLRules(f, aclID, port)
if err != nil {
t.Fatalf("counting ACL rules: %v", err)
}
if n != 1 {
t.Errorf("ACL rules for port %s = %d, want exactly 1 across the toggle", port, n)
}
}