blob: eb0137ba596d9ed788be40971cb57f256ae56e0c [file] [log] [blame] [edit]
/*
Copyright The containerd Authors.
Licensed 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 integration
import (
"fmt"
"strings"
"testing"
"time"
"github.com/containerd/nri/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
)
// Test NRI networking capabilities.
func TestNriPluginNetworkingSynchronization(t *testing.T) {
skipNriTestIfNecessary(t)
t.Log("Test that NRI plugins get pod networking attributes on synchronization.")
var (
tc = &nriTest{
t: t,
}
podCount = 3
ctrPerPod = 2
podPrefix = "pod-net-sync"
)
tc.setup()
for i := 0; i < podCount; i++ {
podID := tc.runPod(fmt.Sprintf("%s%d", podPrefix, i))
for j := 0; j < ctrPerPod; j++ {
tc.startContainer(podID, fmt.Sprintf("ctr%d", j))
}
}
numPods := 0
// override hooks are executed after the sync events, use a channel to synchronize the test
syncCh := make(chan struct{})
plugin := &mockPlugin{
synchronize: func(mp *mockPlugin, pods []*api.PodSandbox, c []*api.Container) ([]*api.ContainerUpdate, error) {
defer close(syncCh)
for _, pod := range pods {
t.Logf("synchronize pod %s/%s: namespace=%s ips=%v", pod.GetNamespace(), pod.GetName(), getNetworkNamespace(pod), pod.GetIps())
// only process the pods created by this test
if !strings.HasPrefix(pod.GetName(), podPrefix) {
t.Logf("DEBUG pod namespace %s name %s tc.namespace %s", pod.GetNamespace(), pod.GetName(), tc.namespace)
continue
}
numPods++
// get the pod network namespace
ns := getNetworkNamespace(pod)
// test only creates pods with network
require.NotEmpty(t, ns)
require.ElementsMatch(t, pod.GetIps(), getNetworkNamespaceIPs(ns))
}
return nil, nil
},
}
tc.connectNewPlugin(plugin)
err := plugin.Wait(PluginSynchronized, time.After(pluginSyncTimeout))
require.NoError(t, err, "plugin sync wait")
for _, id := range tc.pods {
for _, plugin := range tc.plugins {
_, ok := plugin.pods[id]
require.True(tc.t, ok, "runtime sync of pod "+id)
}
}
for _, id := range tc.ctrs {
for _, plugin := range tc.plugins {
_, ok := plugin.ctrs[id]
require.True(t, ok, "runtime sync of container "+id)
}
}
select {
case <-syncCh:
case <-time.After(pluginSyncTimeout):
t.Fatalf("test timed out waiting for the plugin to sync")
}
require.Equal(t, numPods, podCount)
}
func TestNriPluginNetworkingLifecycle(t *testing.T) {
skipNriTestIfNecessary(t)
t.Log("Test that NRI plugins get pod networking attributes during the pod lifecycle.")
var (
tc = &nriTest{
t: t,
}
podName = "pod-net-lifecycle"
)
tc.setup()
hookExecs := 0
// override hooks are executed after the sync events, use channels to synchronize the test
runPodSandboxCh := make(chan struct{})
stopPodSandboxCh := make(chan struct{})
removePodSandboxCh := make(chan struct{})
plugin := &mockPlugin{
runPodSandbox: func(mp *mockPlugin, pod *api.PodSandbox) error {
t.Logf("runPodSandbox pod %s/%s: namespace=%s ips=%v", pod.GetNamespace(), pod.GetName(), getNetworkNamespace(pod), pod.GetIps())
if pod.Name != podName {
return nil
}
defer close(runPodSandboxCh)
// get the pod network namespace
ns := getNetworkNamespace(pod)
// test only creates pods with network
require.NotEmpty(t, ns)
require.ElementsMatch(t, pod.GetIps(), getNetworkNamespaceIPs(ns))
hookExecs++
return nil
},
stopPodSandbox: func(mp *mockPlugin, pod *api.PodSandbox) error {
t.Logf("stopPodSandbox pod %s/%s: namespace=%s ips=%v", pod.GetNamespace(), pod.GetName(), getNetworkNamespace(pod), pod.GetIps())
if pod.Name != podName {
return nil
}
defer close(stopPodSandboxCh)
// get the pod network namespace
ns := getNetworkNamespace(pod)
// test only creates pods with network
require.NotEmpty(t, ns)
require.ElementsMatch(t, pod.GetIps(), getNetworkNamespaceIPs(ns))
hookExecs++
return nil
},
removePodSandbox: func(mp *mockPlugin, pod *api.PodSandbox) error {
t.Logf("removePodSandbox pod %s/%s: namespace=%s ips=%v", pod.GetNamespace(), pod.GetName(), getNetworkNamespace(pod), pod.GetIps())
if pod.Name != podName {
return nil
}
defer close(removePodSandboxCh)
// get the pod network namespace
ns := getNetworkNamespace(pod)
// test only creates pods with network but at this point networking namespace is not present
require.Empty(t, ns)
// the Pod assigned IPs are still available
require.NotEmpty(t, pod.GetIps())
hookExecs++
return nil
},
}
tc.connectNewPlugin(plugin)
err := plugin.Wait(PluginSynchronized, time.After(pluginSyncTimeout))
require.NoError(t, err, "plugin sync wait")
sbConfig := PodSandboxConfig(podName, tc.namespace)
podID, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler)
require.NoError(t, err)
err = plugin.Wait(&Event{Pod: podID, Type: EventType(RunPodSandbox)}, time.After(pluginSyncTimeout))
require.NoError(t, err, "plugin sync wait")
select {
case <-runPodSandboxCh:
case <-time.After(pluginSyncTimeout):
t.Fatalf("test timed out waiting for the RunPodSandbox hook to be executed")
}
assert.NoError(t, runtimeService.StopPodSandbox(podID))
err = plugin.Wait(&Event{Pod: podID, Type: EventType(StopPodSandbox)}, time.After(pluginSyncTimeout))
require.NoError(t, err, "plugin sync wait")
select {
case <-stopPodSandboxCh:
case <-time.After(pluginSyncTimeout):
t.Fatalf("test timed out waiting for the StopPodSandbox hook to be executed")
}
assert.NoError(t, runtimeService.RemovePodSandbox(podID))
err = plugin.Wait(&Event{Pod: podID, Type: EventType(RemovePodSandbox)}, time.After(pluginSyncTimeout))
require.NoError(t, err, "plugin sync wait")
select {
case <-removePodSandboxCh:
case <-time.After(pluginSyncTimeout):
t.Fatalf("test timed out waiting for the RemovePodSandbox hook to be executed")
}
if hookExecs != 3 {
t.Fatalf("expected 3 hooks executed only got %d", hookExecs)
}
}
func getNetworkNamespace(pod *api.PodSandbox) string {
// get the pod network namespace
for _, namespace := range pod.Linux.GetNamespaces() {
if namespace.Type == "network" {
return namespace.Path
}
}
return ""
}
func getNetworkNamespaceIPs(nsPath string) []string {
ips := []string{}
sandboxNs, err := netns.GetFromPath(nsPath)
if err != nil {
return ips
}
// to avoid golang problem with goroutines we create the socket in the
// namespace and use it directly
nhNs, err := netlink.NewHandleAt(sandboxNs)
if err != nil {
return ips
}
// there is a convention the interface inside the Pod is always named eth0
// internal/cri/server/helpers.go: defaultIfName = "eth0"
nsLink, err := nhNs.LinkByName("eth0")
if err != nil {
return ips
}
addrs, err := nhNs.AddrList(nsLink, netlink.FAMILY_ALL)
if err != nil {
return ips
}
for _, addr := range addrs {
// ignore link local and loopback addresses
// those are not added by the CNI
if addr.IP.IsGlobalUnicast() {
ips = append(ips, addr.IP.String())
}
}
return ips
}