Files
ubicloud/rhizome/kubernetes/spec/ubi_cni_spec.rb
Eren Başak 583168feb2 Move CNI logic to lib and improve tests
CNI class is moved to lib folder and the class is only
instantiated and called. All the function and functionalities
are properly tested.

Moved gen_mac and mac_to_ipv6_link_local functions to
rhizome/common/lib because vm_setup.rb and CNI uses these
functions. Duplicate definition of this function is removed from CNI

Co-authored-by: Eren Başak <eren@ubicloud.com>
Co-authored-by: mohi-kalantari <mohi.kalantari1@gmail.com>
2025-02-04 08:56:06 +01:00

250 lines
11 KiB
Ruby

# frozen_string_literal: true
require_relative "../lib/ubi_cni"
require "json"
RSpec.describe UbiCNI do
subject(:ubicni) { described_class.new(input, logger) }
let(:logger) { Logger.new(IO::NULL) }
let(:input) { {"ranges" => {"subnet_ipv4" => "192.168.1.0/24", "subnet_ipv6" => "fd00::/64", "subnet_ula_ipv6" => "fc00::/64"}} }
before do
allow(ENV).to receive(:[]).and_return(nil)
allow(ENV).to receive(:[]).with("CNI_CONTAINERID").and_return("1234")
allow(ENV).to receive(:[]).with("CNI_COMMAND").and_return("xx")
allow(ENV).to receive(:[]).with("CNI_NETNS").and_return("/var/run/netns/test-ns")
allow(ENV).to receive(:[]).with("CNI_IFNAME").and_return("eth0")
allow(ENV).to receive(:[]).with("CNI_ARGS").and_return("xx")
allow(ENV).to receive(:[]).with("CNI_PATH").and_return("xx")
end
describe "#run" do
describe "#handle_add" do
let(:input) do
{
"ranges" => {
"subnet_ula_ipv6" => "fd00::/64",
"subnet_ipv6" => "2001:db8::/64",
"subnet_ipv4" => "192.168.1.0/24"
}
}
end
let(:container_ipv6) { IPAddr.new("2001:db8::2") }
let(:container_ula_ipv6) { IPAddr.new("fd00::2") }
let(:ipv4_container_ip) { IPAddr.new("192.168.1.100") }
let(:ipv4_gateway_ip) { IPAddr.new("192.168.1.1") }
before do
allow(ENV).to receive(:[]).with("CNI_CONTAINERID").and_return("abcdef123456")
allow(ENV).to receive(:[]).with("CNI_COMMAND").and_return("ADD")
allow(ENV).to receive(:[]).with("CNI_NETNS").and_return("/var/run/netns/testnetns")
allow(ENV).to receive(:[]).with("CNI_IFNAME").and_return("eth0")
allow(FileUtils).to receive(:mkdir_p)
allow(File).to receive(:write)
expect(ubicni).to receive(:gen_mac).and_return("00:11:22:33:44:55")
expect(ubicni).to receive(:gen_mac).and_return("00:aa:bb:cc:dd:ee")
allow(ubicni).to receive(:mac_to_ipv6_link_local).with("00:11:22:33:44:55").and_return("fe80::02aa:bbff:fecc:ddee")
allow(ubicni).to receive(:mac_to_ipv6_link_local).with("00:aa:bb:cc:dd:ee").and_return("fe80::0211:22ff:fe33:4455")
allow(ubicni).to receive(:find_random_available_ip).and_return(
container_ipv6, container_ula_ipv6, ipv4_container_ip, ipv4_gateway_ip
)
end
it "sets up networking and assigns IPs correctly" do
expect(ubicni).to receive(:r).with("ip link add veth_abcdef12 addr 00:aa:bb:cc:dd:ee type veth peer name eth0 addr 00:11:22:33:44:55 netns testnetns").ordered
expect(ubicni).to receive(:r).with("ip -6 -n testnetns addr replace #{container_ipv6}/#{container_ipv6.prefix} dev eth0").ordered
expect(ubicni).to receive(:r).with("ip -6 -n testnetns link set eth0 mtu 1400 up").ordered
expect(ubicni).to receive(:r).with("ip -6 -n testnetns route replace default via fe80::0211:22ff:fe33:4455 dev eth0").ordered
expect(ubicni).to receive(:r).with("ip -6 link set veth_abcdef12 mtu 1400 up").ordered
expect(ubicni).to receive(:r).with("ip -6 route replace #{container_ipv6}/#{container_ipv6.prefix} via fe80::02aa:bbff:fecc:ddee dev veth_abcdef12 mtu 1400").ordered
expect(ubicni).to receive(:r).with("ip -6 -n testnetns addr replace #{container_ula_ipv6}/#{container_ula_ipv6.prefix} dev eth0").ordered
expect(ubicni).to receive(:r).with("ip -6 -n testnetns link set eth0 mtu 1400 up").ordered
expect(ubicni).to receive(:r).with("ip -6 link set veth_abcdef12 mtu 1400 up").ordered
expect(ubicni).to receive(:r).with("ip -6 route replace #{container_ula_ipv6}/#{container_ula_ipv6.prefix} via fe80::02aa:bbff:fecc:ddee dev veth_abcdef12 mtu 1400").ordered
expect(ubicni).to receive(:r).with("ip addr replace #{ipv4_gateway_ip}/24 dev veth_abcdef12").ordered
expect(ubicni).to receive(:r).with("ip link set veth_abcdef12 mtu 1400 up").ordered
expect(ubicni).to receive(:r).with("ip -n testnetns addr replace #{ipv4_container_ip}/24 dev eth0").ordered
expect(ubicni).to receive(:r).with("ip -n testnetns link set eth0 mtu 1400 up").ordered
expect(ubicni).to receive(:r).with("ip -n testnetns route replace default via #{ipv4_gateway_ip}").ordered
expect(ubicni).to receive(:r).with("ip route replace #{ipv4_container_ip}/#{ipv4_container_ip.prefix} via #{ipv4_gateway_ip} dev veth_abcdef12").ordered
expect(ubicni).to receive(:r).with("echo 1 > /proc/sys/net/ipv4/conf/veth_abcdef12/proxy_arp").ordered
output = ubicni.handle_add
response = JSON.parse(output)
expect(response).to include("cniVersion" => "1.0.0")
expect(response["interfaces"]).to be_an(Array)
expect(response["interfaces"]).to include(
hash_including("name" => "eth0", "mac" => "00:11:22:33:44:55")
)
expect(response["ips"]).to be_an(Array)
expect(response["ips"]).to include(
hash_including("address" => "#{container_ipv6}/128"),
hash_including("address" => "#{container_ula_ipv6}/128"),
hash_including("address" => "#{ipv4_container_ip}/32")
)
expect(response["routes"]).to be_an(Array)
expect(response["routes"]).to include(
hash_including("dst" => "0.0.0.0/0")
)
end
end
it "calls handle_add if CNI_COMMAND is ADD" do
allow(ENV).to receive(:[]).with("CNI_COMMAND").and_return("ADD")
expect(ubicni).to receive(:handle_add).and_return(nil)
ubicni.run
end
it "calls handle_del if CNI_COMMAND is DEL" do
allow(ENV).to receive(:[]).with("CNI_COMMAND").and_return("DEL")
expect(ubicni).to receive(:handle_del).and_return(nil)
ubicni.run
end
it "calls handle_get if CNI_COMMAND is GET" do
allow(ENV).to receive(:[]).with("CNI_COMMAND").and_return("GET")
expect(ubicni).to receive(:handle_get).and_return(nil)
ubicni.run
end
it "raises an error for an unsupported command" do
allow(ENV).to receive(:[]).with("CNI_COMMAND").and_return("INVALID")
expect(logger).to receive(:error).with("Unsupported CNI command: INVALID")
expect { ubicni.run }.to output("{\"code\":100,\"msg\":\"Unsupported CNI command: INVALID\"}\n").to_stdout.and(raise_error(SystemExit))
end
end
describe "#handle_del" do
it "removes allocated IP when container exists" do
ubicni.instance_variable_set(:@ipam_store, {"allocated_ips" => {"1234" => ["192.168.1.2"]}})
expect(File).to receive(:write)
expect { ubicni.handle_del }.to change { ubicni.instance_variable_get(:@ipam_store)["allocated_ips"].size }.by(-1)
end
it "does nothing when container does not exist" do
ubicni.instance_variable_set(:@ipam_store, {"allocated_ips" => {"12345" => ["192.168.1.2"]}})
expect(File).not_to receive(:write)
expect { ubicni.handle_del }.not_to change { ubicni.instance_variable_get(:@ipam_store)["allocated_ips"].size }
end
end
describe "#handle_get" do
before do
allow(File).to receive(:read).with("/opt/cni/bin/ubicni-ipam-store").and_return("{}")
allow(File).to receive(:exist?).with("/opt/cni/bin/ubicni-ipam-store").and_return(true)
allow(File).to receive(:exist?).with("/etc/netns/test-ns/resolv.conf").and_return(true)
allow(File).to receive(:readlines).and_return(["nameserver 8.8.8.8", "search local"])
allow(ubicni).to receive(:r).and_return("link/ether 00:11:22:33:44:55", "inet6 fd00::1/64")
end
it "retrieves container network information" do
allow(ENV).to receive(:[]).with("CNI_COMMAND").and_return("GET")
allow(ENV).to receive(:[]).with("CNI_NETNS").and_return("/var/run/netns/test-ns")
allow(ENV).to receive(:[]).with("CNI_IFNAME").and_return("eth0")
allow(ubicni).to receive(:r).with("ip -n test-ns link show eth0").and_return(<<~OUTPUT)
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff
OUTPUT
allow(ubicni).to receive(:r).with("ip -n test-ns -6 addr show dev eth0").and_return(<<~OUTPUT)
inet6 2001:db8::2/64 scope global
valid_lft forever preferred_lft forever
OUTPUT
dns_config_path = "/etc/netns/test-ns/resolv.conf"
allow(File).to receive(:exist?).with(dns_config_path).and_return(true)
allow(File).to receive(:readlines).with(dns_config_path).and_return([
"nameserver 10.96.0.10\n",
"search default.svc.cluster.local svc.cluster.local cluster.local\n",
"options ndots:5\n"
])
output = ubicni.handle_get
response = JSON.parse(output)
expect(response).to include("cniVersion" => "1.0.0")
expect(response["interfaces"]).to be_an(Array)
expect(response["interfaces"]).to include(
hash_including("name" => "eth0", "mac" => "00:11:22:33:44:55")
)
expect(response["ips"]).to be_an(Array)
expect(response["ips"]).to include(
hash_including("address" => "2001:db8::2/64")
)
expect(response["dns"]["nameservers"]).to eq(["10.96.0.10"])
expect(response["dns"]["search"]).to eq("default.svc.cluster.local svc.cluster.local cluster.local".split)
end
it "returns empty handed if the dns config file does not exist" do
dns_config_path = "/etc/netns/test-ns/resolv.conf"
allow(File).to receive(:exist?).with(dns_config_path).and_return(false)
response = JSON.parse(ubicni.handle_get)
expect(response["dns"]["nameservers"]).to eq([])
expect(response["dns"]["search"]).to eq([])
end
end
describe "#gen_mac" do
it "generates a valid MAC address" do
expect(gen_mac).to match(/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/)
end
end
describe "#find_random_available_ip" do
let(:subnet) { IPAddr.new("192.168.1.0/24") }
let(:subnetipv6) { IPAddr.new("fd00::/64") }
it "returns an IP address within the subnet" do
ip = ubicni.find_random_available_ip(subnet)
expect(subnet.include?(ip)).to be true
end
it "returns an IP address within the IPv6 subnet" do
ip = ubicni.find_random_available_ip(subnetipv6)
expect(subnetipv6.include?(ip)).to be true
end
it "raises an error when all available IPs are allocated" do
all_ips = (1..254).map { |i| "192.168.1.#{i}" }
ubicni.instance_variable_set(:@ipam_store, {"allocated_ips" => {"test-container" => all_ips}})
expect { ubicni.find_random_available_ip(subnet) }.to raise_error(RuntimeError, /Could not find an available IP after 100 retries/)
end
end
describe "#mac_to_ipv6_link_local" do
it "converts a MAC address to an IPv6 link-local address" do
mac_address = "00:11:22:33:44:55"
expected_ipv6 = "fe80::0211:22ff:fe33:4455"
expect(mac_to_ipv6_link_local(mac_address)).to eq(expected_ipv6)
end
end
describe "#calculate_subnet_size" do
it "calculates subnet size for an IPv4 subnet" do
subnet = IPAddr.new("192.168.1.0/24")
expect(ubicni.calculate_subnet_size(subnet)).to eq(256)
end
it "calculates subnet size for an IPv6 subnet" do
subnet = IPAddr.new("fd00::/64")
expect(ubicni.calculate_subnet_size(subnet)).to eq(2**64)
end
end
end