Skip to content

Commit a323b42

Browse files
committed
e2e test: introduce a go framework
This commit adds a minimal golang test framework to check the MVP functionality of the project. Signed-off-by: Miguel Duarte Barroso <[email protected]>
1 parent 62860a5 commit a323b42

File tree

5 files changed

+560
-1
lines changed

5 files changed

+560
-1
lines changed

.github/workflows/e2e-container.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ jobs:
3131
- name: Test - provisioning the examples
3232
run: e2e/test-provisioning-examples.sh
3333

34+
- name: Test - execute golang based e2e tests
35+
env:
36+
KUBECONFIG: /home/runner/.kube/config
37+
run: make e2e/test
38+
3439
- name: Cleanup cluster
3540
run: |
3641
kind delete cluster # gracefully remove the cluster

Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ manifests:
2424
IMAGE_REGISTRY=${IMAGE_REGISTRY} CRI_SOCKET_PATH=${CRI_SOCKET_PATH} hack/generate_manifests.sh
2525

2626
test:
27-
$(GO) test -v ./...
27+
$(GO) test -v ./pkg/...
28+
29+
e2e/test:
30+
$(GO) test -v -count=1 ./e2e/...

e2e/client/types.go

+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net"
9+
"time"
10+
11+
corev1 "k8s.io/api/core/v1"
12+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/util/wait"
15+
"k8s.io/client-go/kubernetes"
16+
"k8s.io/client-go/rest"
17+
18+
nettypes "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
19+
netclient "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/typed/k8s.cni.cncf.io/v1"
20+
21+
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/annotations"
22+
)
23+
24+
type E2EClient struct {
25+
k8sClient kubernetes.Interface
26+
netAttachDefClient netclient.K8sCniCncfIoV1Interface
27+
}
28+
29+
func New(config *rest.Config) (*E2EClient, error) {
30+
clientSet, err := kubernetes.NewForConfig(config)
31+
if err != nil {
32+
return nil, err
33+
}
34+
netClient, err := netclient.NewForConfig(config)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
return &E2EClient{
40+
k8sClient: clientSet,
41+
netAttachDefClient: netClient,
42+
}, nil
43+
}
44+
45+
func (c *E2EClient) AddNetAttachDef(netattach *nettypes.NetworkAttachmentDefinition) (*nettypes.NetworkAttachmentDefinition, error) {
46+
return c.netAttachDefClient.NetworkAttachmentDefinitions(netattach.ObjectMeta.Namespace).Create(context.TODO(), netattach, metav1.CreateOptions{})
47+
}
48+
49+
func (c *E2EClient) DelNetAttachDef(namespace string, podName string) error {
50+
return c.netAttachDefClient.NetworkAttachmentDefinitions(namespace).Delete(context.TODO(), podName, metav1.DeleteOptions{})
51+
}
52+
53+
func (c *E2EClient) AddNamespace(name string) (*corev1.Namespace, error) {
54+
return c.k8sClient.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
55+
ObjectMeta: metav1.ObjectMeta{
56+
Name: name,
57+
},
58+
}, metav1.CreateOptions{})
59+
}
60+
61+
func (c *E2EClient) DeleteNamespace(name string) error {
62+
const timeout = 30 * time.Second
63+
64+
if err := c.k8sClient.CoreV1().Namespaces().Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
65+
return err
66+
}
67+
if err := wait.PollImmediate(time.Second, timeout, func() (done bool, err error) {
68+
if _, err := c.k8sClient.CoreV1().Namespaces().Get(context.Background(), name, metav1.GetOptions{}); err != nil && k8serrors.IsNotFound(err) {
69+
return true, nil
70+
} else if err != nil {
71+
return false, err
72+
}
73+
return false, nil
74+
}); err != nil {
75+
return err
76+
}
77+
return nil
78+
}
79+
80+
func (c *E2EClient) ProvisionPod(podName string, namespace string, label, annotations map[string]string) (*corev1.Pod, error) {
81+
pod := PodObject(podName, namespace, label, annotations)
82+
pod, err := c.k8sClient.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{})
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
const podCreateTimeout = 10 * time.Second
88+
if err := c.WaitForPodReady(pod.Namespace, pod.Name, podCreateTimeout); err != nil {
89+
return nil, err
90+
}
91+
92+
pod, err = c.k8sClient.CoreV1().Pods(pod.Namespace).Get(context.Background(), pod.Name, metav1.GetOptions{})
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
return pod, nil
98+
}
99+
100+
func (c *E2EClient) DeletePod(pod *corev1.Pod) error {
101+
if err := c.k8sClient.CoreV1().Pods(pod.Namespace).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{}); err != nil {
102+
return err
103+
}
104+
105+
const podDeleteTimeout = 20 * time.Second
106+
if err := c.WaitForPodToDisappear(pod.GetNamespace(), pod.GetName(), podDeleteTimeout); err != nil {
107+
return err
108+
}
109+
return nil
110+
}
111+
112+
func (c *E2EClient) AddNetworkToPod(pod *corev1.Pod, networkName string, namespace string, ifaceToAdd string) error {
113+
pod.ObjectMeta.Annotations[nettypes.NetworkAttachmentAnnot] = dynamicNetworksAnnotation(pod, networkName, "ns1", ifaceToAdd, nil)
114+
_, err := c.k8sClient.CoreV1().Pods(namespace).Update(context.TODO(), pod, metav1.UpdateOptions{})
115+
return err
116+
}
117+
118+
func (c *E2EClient) RemoveNetworkFromPod(pod *corev1.Pod, networkName string, namespace string, ifaceToRemove string) error {
119+
pod.ObjectMeta.Annotations[nettypes.NetworkAttachmentAnnot] = removeFromDynamicNetworksAnnotation(pod, networkName, namespace, ifaceToRemove)
120+
_, err := c.k8sClient.CoreV1().Pods(namespace).Update(context.TODO(), pod, metav1.UpdateOptions{})
121+
return err
122+
}
123+
124+
// WaitForPodReady polls up to timeout seconds for pod to enter steady state (running or succeeded state).
125+
// Returns an error if the pod never enters a steady state.
126+
func (c *E2EClient) WaitForPodReady(namespace, podName string, timeout time.Duration) error {
127+
return wait.PollImmediate(time.Second, timeout, isPodRunning(c.k8sClient, podName, namespace))
128+
}
129+
130+
// WaitForPodToDisappear polls up to timeout seconds for pod to be gone from the Kubernetes cluster.
131+
// Returns an error if the pod is never deleted, or if GETing it returns an error other than `NotFound`.
132+
func (c *E2EClient) WaitForPodToDisappear(namespace, podName string, timeout time.Duration) error {
133+
return wait.PollImmediate(time.Second, timeout, isPodGone(c.k8sClient, podName, namespace))
134+
}
135+
136+
// WaitForPodBySelector waits up to timeout seconds for all pods in 'namespace' with given 'selector' to enter provided state
137+
// If no pods are found, return nil.
138+
func (c *E2EClient) WaitForPodBySelector(namespace, selector string, timeout time.Duration) error {
139+
podList, err := c.ListPods(namespace, selector)
140+
if err != nil {
141+
return err
142+
}
143+
144+
if len(podList.Items) == 0 {
145+
return nil
146+
}
147+
148+
for _, pod := range podList.Items {
149+
if err := c.WaitForPodReady(namespace, pod.Name, timeout); err != nil {
150+
return err
151+
}
152+
}
153+
return nil
154+
}
155+
156+
// ListPods returns the list of currently scheduled or running pods in `namespace` with the given selector
157+
func (c *E2EClient) ListPods(namespace, selector string) (*corev1.PodList, error) {
158+
listOptions := metav1.ListOptions{LabelSelector: selector}
159+
podList, err := c.k8sClient.CoreV1().Pods(namespace).List(context.Background(), listOptions)
160+
161+
if err != nil {
162+
return nil, err
163+
}
164+
return podList, nil
165+
}
166+
167+
func isPodRunning(cs kubernetes.Interface, podName, namespace string) wait.ConditionFunc {
168+
return func() (bool, error) {
169+
pod, err := cs.CoreV1().Pods(namespace).Get(context.Background(), podName, metav1.GetOptions{})
170+
if err != nil {
171+
return false, err
172+
}
173+
174+
switch pod.Status.Phase {
175+
case corev1.PodRunning:
176+
return true, nil
177+
case corev1.PodFailed:
178+
return false, errors.New("pod failed")
179+
case corev1.PodSucceeded:
180+
return false, errors.New("pod succeeded")
181+
}
182+
183+
return false, nil
184+
}
185+
}
186+
187+
func isPodGone(cs kubernetes.Interface, podName, namespace string) wait.ConditionFunc {
188+
return func() (bool, error) {
189+
pod, err := cs.CoreV1().Pods(namespace).Get(context.Background(), podName, metav1.GetOptions{})
190+
if err != nil && k8serrors.IsNotFound(err) {
191+
return true, nil
192+
} else if err != nil {
193+
return false, fmt.Errorf("something weird happened with the pod, which is in state: [%s]. Errors: %w", pod.Status.Phase, err)
194+
}
195+
196+
return false, nil
197+
}
198+
}
199+
200+
func PodObject(podName string, namespace string, label, annotations map[string]string) *corev1.Pod {
201+
return &corev1.Pod{
202+
ObjectMeta: podMeta(podName, namespace, label, annotations),
203+
Spec: podSpec("samplepod"),
204+
}
205+
}
206+
207+
func podSpec(containerName string) corev1.PodSpec {
208+
const testImage = "k8s.gcr.io/e2e-test-images/agnhost:2.26"
209+
return corev1.PodSpec{
210+
Containers: []corev1.Container{
211+
{
212+
Name: containerName,
213+
Command: containerCmd(),
214+
Image: testImage,
215+
},
216+
},
217+
}
218+
}
219+
220+
func containerCmd() []string {
221+
return []string{"/bin/ash", "-c", "trap : TERM INT; sleep infinity & wait"}
222+
}
223+
224+
func podMeta(podName string, namespace string, label map[string]string, annotations map[string]string) metav1.ObjectMeta {
225+
return metav1.ObjectMeta{
226+
Name: podName,
227+
Namespace: namespace,
228+
Labels: label,
229+
Annotations: annotations,
230+
}
231+
}
232+
233+
func dynamicNetworksAnnotation(pod *corev1.Pod, networkName string, netNamespace string, ifaceName string, ip *net.IP) string {
234+
currentNetworkSelectionElementsString, wasFound := pod.ObjectMeta.Annotations[nettypes.NetworkAttachmentAnnot]
235+
if !wasFound {
236+
return ""
237+
}
238+
239+
currentNetworkSelectionElements, err := annotations.ParsePodNetworkAnnotations(currentNetworkSelectionElementsString, netNamespace)
240+
if err != nil {
241+
return ""
242+
}
243+
244+
var ips []string
245+
if ip != nil {
246+
ips = []string{ip.String()}
247+
}
248+
updatedNetworkSelectionElements := append(
249+
currentNetworkSelectionElements,
250+
&nettypes.NetworkSelectionElement{
251+
Name: networkName,
252+
Namespace: netNamespace,
253+
InterfaceRequest: ifaceName,
254+
IPRequest: ips,
255+
},
256+
)
257+
newSelectionElements, err := json.Marshal(updatedNetworkSelectionElements)
258+
if err != nil {
259+
return ""
260+
}
261+
262+
return string(newSelectionElements)
263+
}
264+
265+
func removeFromDynamicNetworksAnnotation(pod *corev1.Pod, networkName string, netNamespace string, ifaceName string) string {
266+
currentNetworkSelectionElementsString, wasFound := pod.ObjectMeta.Annotations[nettypes.NetworkAttachmentAnnot]
267+
if !wasFound {
268+
return ""
269+
}
270+
271+
currentNetworkSelectionElements, err := annotations.ParsePodNetworkAnnotations(currentNetworkSelectionElementsString, netNamespace)
272+
if err != nil {
273+
return ""
274+
}
275+
276+
var updatedNetworkSelectionElements []nettypes.NetworkSelectionElement
277+
for i := range currentNetworkSelectionElements {
278+
if currentNetworkSelectionElements[i].Name == networkName && currentNetworkSelectionElements[i].Namespace == netNamespace && currentNetworkSelectionElements[i].InterfaceRequest == ifaceName {
279+
continue
280+
}
281+
updatedNetworkSelectionElements = append(updatedNetworkSelectionElements, *currentNetworkSelectionElements[i])
282+
}
283+
284+
var newSelectionElements string
285+
if len(updatedNetworkSelectionElements) > 0 {
286+
newSelectionElementsBytes, err := json.Marshal(updatedNetworkSelectionElements)
287+
if err != nil {
288+
return ""
289+
}
290+
newSelectionElements = string(newSelectionElementsBytes)
291+
} else {
292+
newSelectionElements = "[]"
293+
}
294+
return newSelectionElements
295+
}

0 commit comments

Comments
 (0)