diff --git a/src/pkg/cluster/injector.go b/src/pkg/cluster/injector.go index 3abd010b8f..9bafbc688a 100644 --- a/src/pkg/cluster/injector.go +++ b/src/pkg/cluster/injector.go @@ -73,7 +73,7 @@ func (c *Cluster) StartInjectionMadness(ctx context.Context, tmpDir string, imag } spinner.Updatef("Creating the injector configmap") - if err = c.createInjectorConfigmap(ctx, tmp.InjectionBinary); err != nil { + if err = c.createInjectorConfigMap(ctx, tmp.InjectionBinary); err != nil { spinner.Fatalf(err, "Unable to create the injector configmap") } @@ -285,7 +285,7 @@ func (c *Cluster) injectorIsReady(ctx context.Context, seedImages []transform.Im return true } -func (c *Cluster) createInjectorConfigmap(ctx context.Context, binaryPath string) error { +func (c *Cluster) createInjectorConfigMap(ctx context.Context, binaryPath string) error { var err error configData := make(map[string][]byte) @@ -473,10 +473,8 @@ func (c *Cluster) getImagesAndNodesForInjection(ctx context.Context) (imageNodeM continue } - for _, taint := range nodeDetails.Spec.Taints { - if taint.Effect == corev1.TaintEffectNoSchedule || taint.Effect == corev1.TaintEffectNoExecute { - continue - } + if hasBlockingTaints(nodeDetails.Spec.Taints) { + continue } for _, container := range pod.Spec.InitContainers { @@ -499,3 +497,12 @@ func (c *Cluster) getImagesAndNodesForInjection(ctx context.Context) (imageNodeM } } } + +func hasBlockingTaints(taints []corev1.Taint) bool { + for _, taint := range taints { + if taint.Effect == corev1.TaintEffectNoSchedule || taint.Effect == corev1.TaintEffectNoExecute { + return true + } + } + return false +} diff --git a/src/pkg/cluster/injector_test.go b/src/pkg/cluster/injector_test.go new file mode 100644 index 0000000000..278549d035 --- /dev/null +++ b/src/pkg/cluster/injector_test.go @@ -0,0 +1,201 @@ +package cluster + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/defenseunicorns/zarf/src/pkg/k8s" +) + +func TestCreateInjectorConfigMap(t *testing.T) { + t.Parallel() + + binData := []byte("foobar") + binPath := filepath.Join(t.TempDir(), "bin") + err := os.WriteFile(binPath, binData, 0o644) + require.NoError(t, err) + + cs := fake.NewSimpleClientset() + c := &Cluster{ + &k8s.K8s{ + Clientset: cs, + }, + } + + ctx := context.Background() + for i := 0; i < 2; i++ { + err = c.createInjectorConfigMap(ctx, binPath) + require.NoError(t, err) + cm, err := cs.CoreV1().ConfigMaps(ZarfNamespaceName).Get(ctx, "rust-binary", metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, binData, cm.BinaryData["zarf-injector"]) + } +} + +func TestCreateService(t *testing.T) { + t.Parallel() + + cs := fake.NewSimpleClientset() + c := &Cluster{ + &k8s.K8s{ + Clientset: cs, + }, + } + + expected, err := os.ReadFile("./testdata/expected-injection-service.json") + require.NoError(t, err) + ctx := context.Background() + for i := 0; i < 2; i++ { + _, err := c.createService(ctx) + require.NoError(t, err) + svc, err := cs.CoreV1().Services(ZarfNamespaceName).Get(ctx, "zarf-injector", metav1.GetOptions{}) + require.NoError(t, err) + b, err := json.Marshal(svc) + require.NoError(t, err) + require.Equal(t, strings.TrimSpace(string(expected)), string(b)) + } +} + +func TestBuildInjectionPod(t *testing.T) { + t.Parallel() + + c := &Cluster{} + pod, err := c.buildInjectionPod("injection-node", "docker.io/library/ubuntu:latest", []string{"foo", "bar"}, "shasum") + require.NoError(t, err) + b, err := json.Marshal(pod) + require.NoError(t, err) + expected, err := os.ReadFile("./testdata/expected-injection-pod.json") + require.NoError(t, err) + require.Equal(t, strings.TrimSpace(string(expected)), string(b)) +} + +func TestImagesAndNodesForInjection(t *testing.T) { + t.Parallel() + + ctx := context.Background() + cs := fake.NewSimpleClientset() + + c := &Cluster{ + &k8s.K8s{ + Clientset: cs, + Log: func(s string, a ...any) {}, + }, + } + + nodes := []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "no-resources", + }, + Status: corev1.NodeStatus{ + Allocatable: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("400m"), + corev1.ResourceMemory: resource.MustParse("50Mi"), + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "no-schedule-taint", + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + Status: corev1.NodeStatus{ + Allocatable: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1000m"), + corev1.ResourceMemory: resource.MustParse("10Gi"), + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "good", + }, + Status: corev1.NodeStatus{ + Allocatable: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1000m"), + corev1.ResourceMemory: resource.MustParse("10Gi"), + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "no-execute-taint", + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Effect: corev1.TaintEffectNoExecute, + }, + }, + }, + Status: corev1.NodeStatus{ + Allocatable: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1000m"), + corev1.ResourceMemory: resource.MustParse("10Gi"), + }, + }, + }, + } + for i, node := range nodes { + _, err := cs.CoreV1().Nodes().Create(ctx, &node, metav1.CreateOptions{}) + require.NoError(t, err) + podName := fmt.Sprintf("pod-%d", i) + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: node.ObjectMeta.Name, + InitContainers: []corev1.Container{ + { + Image: podName + "-init", + }, + }, + Containers: []corev1.Container{ + { + Image: podName + "-container", + }, + }, + EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Image: podName + "-ephemeral", + }, + }, + }, + }, + } + _, err = cs.CoreV1().Pods(pod.Namespace).Create(ctx, &pod, metav1.CreateOptions{}) + require.NoError(t, err) + } + + getCtx, getCancel := context.WithTimeout(ctx, 1*time.Second) + defer getCancel() + result, err := c.getImagesAndNodesForInjection(getCtx) + require.NoError(t, err) + expected := imageNodeMap{ + "pod-2-init": []string{"good"}, + "pod-2-container": []string{"good"}, + "pod-2-ephemeral": []string{"good"}, + } + require.Equal(t, expected, result) +} diff --git a/src/pkg/cluster/testdata/expected-injection-pod.json b/src/pkg/cluster/testdata/expected-injection-pod.json new file mode 100644 index 0000000000..30f2e5b1f1 --- /dev/null +++ b/src/pkg/cluster/testdata/expected-injection-pod.json @@ -0,0 +1 @@ +{"kind":"Pod","apiVersion":"v1","metadata":{"name":"injector","namespace":"zarf","creationTimestamp":null,"labels":{"app":"zarf-injector","zarf.dev/agent":"ignore"}},"spec":{"volumes":[{"name":"init","configMap":{"name":"rust-binary","defaultMode":511}},{"name":"seed","emptyDir":{}},{"name":"foo","configMap":{"name":"foo"}},{"name":"bar","configMap":{"name":"bar"}}],"containers":[{"name":"injector","image":"docker.io/library/ubuntu:latest","command":["/zarf-init/zarf-injector","shasum"],"workingDir":"/zarf-init","resources":{"limits":{"cpu":"1","memory":"256Mi"},"requests":{"cpu":"500m","memory":"64Mi"}},"volumeMounts":[{"name":"init","mountPath":"/zarf-init/zarf-injector","subPath":"zarf-injector"},{"name":"seed","mountPath":"/zarf-seed"},{"name":"foo","mountPath":"/zarf-init/foo","subPath":"foo"},{"name":"bar","mountPath":"/zarf-init/bar","subPath":"bar"}],"readinessProbe":{"httpGet":{"path":"/v2/","port":5000},"periodSeconds":2,"successThreshold":1,"failureThreshold":10},"imagePullPolicy":"IfNotPresent"}],"restartPolicy":"Never","nodeName":"injection-node"},"status":{}} diff --git a/src/pkg/cluster/testdata/expected-injection-service.json b/src/pkg/cluster/testdata/expected-injection-service.json new file mode 100644 index 0000000000..dd826cbb4c --- /dev/null +++ b/src/pkg/cluster/testdata/expected-injection-service.json @@ -0,0 +1 @@ +{"kind":"Service","apiVersion":"v1","metadata":{"name":"zarf-injector","namespace":"zarf","creationTimestamp":null},"spec":{"ports":[{"port":5000,"targetPort":0}],"selector":{"app":"zarf-injector"},"type":"NodePort"},"status":{"loadBalancer":{}}}