From d0fe83a20faafef1b36fcfffd7bfcc7a36e1a191 Mon Sep 17 00:00:00 2001 From: adrianc Date: Wed, 30 Oct 2024 17:25:15 +0200 Subject: [PATCH] Add E2E tests add E2E tests for maintenance operator. Signed-off-by: adrianc --- Makefile | 13 +- go.mod | 2 +- test/e2e/e2e_suite_test.go | 124 +++++++++++++++++-- test/e2e/e2e_test.go | 240 ++++++++++++++++++++++--------------- test/utils/utils.go | 148 +++++------------------ 5 files changed, 303 insertions(+), 224 deletions(-) diff --git a/Makefile b/Makefile index 3926023..4f892cf 100644 --- a/Makefile +++ b/Makefile @@ -194,6 +194,13 @@ $(MINIKUBE): | $(LOCALBIN) chmod +x $(MINIKUBE);\ } +# kind is used to set-up local kubernetes cluster for e2e tests. +KIND_VER := v0.24.0 +KIND := $(abspath $(LOCALBIN)/kind-$(KIND_VER)) +.PHONY: kind ## Download kind locally if necessary. + @ test -s $(LOCALBIN)/$(KIND) || GOBIN=$(LOCALBIN) go install sigs.k8s.io/kind$(KIND_VER) + + HELM := $(abspath $(LOCALBIN)/helm) .PHONY: helm helm: $(HELM) ## Download helm (last release) locally if necessary. @@ -276,10 +283,10 @@ test: unit-test lint unit-test: envtest ## Run unit tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -cover -covermode=$(COVER_MODE) -coverprofile=$(COVER_PROFILE) $(PKGS) -# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. -.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. + +.PHONY: test-e2e # Run the e2e tests against a k8s instance with maintenance-operator installed. test-e2e: - go test ./test/e2e/ -v -ginkgo.v + go test ./test/e2e/ -v -ginkgo.v -e2e.maintenanceOperatorNamespace=maintenance-operator .PHONY: lint lint: golangci-lint ## Run golangci-lint linter & yamllint diff --git a/go.mod b/go.mod index 29b2467..0562c3d 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( k8s.io/kubectl v0.31.2 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/controller-runtime v0.19.1 + sigs.k8s.io/yaml v1.4.0 ) require ( @@ -106,5 +107,4 @@ require ( sigs.k8s.io/kustomize/api v0.17.2 // indirect sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index a15cfad..1d4994f 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -1,32 +1,136 @@ /* -Copyright 2024. + Copyright 2024, NVIDIA CORPORATION & AFFILIATES -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 + 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 + 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. + 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" + "flag" "fmt" + "os" + "path/filepath" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + maintenancev1 "github.com/Mellanox/maintenance-operator/api/v1alpha1" +) + +type testClusterInfo struct { + workerNodes []string + controlPlaneNodes []string +} + +// discoverNodes discovers control plane and worker nodes in the cluster. +// then stores them in testClusterInfo +func (tc *testClusterInfo) discoverNodes(k8sClient client.Client) error { + nodes := &corev1.NodeList{} + if err := k8sClient.List(testContext, nodes); err != nil { + return errors.Wrap(err, "failed to list nodes") + } + + for _, node := range nodes.Items { + if _, ok := node.Labels["node-role.kubernetes.io/control-plane"]; ok { + tc.controlPlaneNodes = append(tc.controlPlaneNodes, node.Name) + } + if _, ok := node.Labels["node-role.kubernetes.io/worker"]; ok { + tc.workerNodes = append(tc.workerNodes, node.Name) + } + } + + if len(tc.controlPlaneNodes) == 0 { + return fmt.Errorf("no control plane nodes found") + } + + if len(tc.workerNodes) == 0 { + return fmt.Errorf("no worker nodes found") + } + + return nil +} + +var ( + // testContext is a context for testing. + testContext context.Context + // k8sClient is a Kubernetes client. + k8sClient client.Client + // testKubeconfig is the path to the kubeconfig file used for testing. + testKubeconfig string + // testCluster contains cluster information + testCluster testClusterInfo + // maintenanceOperatorNamespace is the namespace where the maintenance operator is installed. + maintenanceOperatorNamespace string ) +func init() { + flag.StringVar(&testKubeconfig, "e2e.kubeconfig", "", "path to the kubeconfig file used for testing") + flag.StringVar(&maintenanceOperatorNamespace, "e2e.maintenanceOperatorNamespace", "maintenance-operator", "namespace where the maintenance operator is installed") +} + // Run e2e tests using the Ginkgo runner. func TestE2E(t *testing.T) { RegisterFailHandler(Fail) fmt.Fprintf(GinkgoWriter, "Starting maintenance-operator suite\n") RunSpecs(t, "e2e suite") } + +var _ = BeforeSuite(func() { + fmt.Fprintf(GinkgoWriter, "BeforeSuite\n") + // set up context + testContext = ctrl.SetupSignalHandler() + + // set up logger + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + + // setup scheme + fmt.Fprintf(GinkgoWriter, "setup scheme\n") + s := runtime.NewScheme() + Expect(scheme.AddToScheme(s)).To(Succeed()) + Expect(maintenancev1.AddToScheme(s)).To(Succeed()) + + // if testKubeconfig is not set, default it to $HOME/.kube/config + fmt.Fprintf(GinkgoWriter, "get kubeconfig\n") + home, exists := os.LookupEnv("HOME") + Expect(exists).To(BeTrue()) + if testKubeconfig == "" { + testKubeconfig = filepath.Join(home, ".kube/config") + } + + // create k8sClient + fmt.Fprintf(GinkgoWriter, "create client\n") + restConfig, err := clientcmd.BuildConfigFromFlags("", testKubeconfig) + Expect(err).NotTo(HaveOccurred()) + k8sClient, err = client.New(restConfig, client.Options{Scheme: s}) + Expect(err).NotTo(HaveOccurred()) + + // discover nodes + fmt.Fprintf(GinkgoWriter, "discover nodes\n") + Expect(testCluster.discoverNodes(k8sClient)).To(Succeed()) + + fmt.Fprintf(GinkgoWriter, "Cluster Information\n") + fmt.Fprintf(GinkgoWriter, "ControlPlane Nodes: %+v\n", testCluster.controlPlaneNodes) + fmt.Fprintf(GinkgoWriter, "Worker Nodes: %+v\n", testCluster.workerNodes) + fmt.Fprintf(GinkgoWriter, "BeforeSuite End\n") +}) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index ef3180c..719c266 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -1,121 +1,173 @@ /* -Copyright 2024. + Copyright 2024, NVIDIA CORPORATION & AFFILIATES -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 + 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 + 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. + 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 ( "fmt" - "os/exec" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/Mellanox/maintenance-operator/test/utils" -) + maintenancev1 "github.com/Mellanox/maintenance-operator/api/v1alpha1" + "github.com/Mellanox/maintenance-operator/internal/k8sutils" -const namespace = "maintenance-operator-system" + e2eutils "github.com/Mellanox/maintenance-operator/test/utils" +) -var _ = Describe("controller", Ordered, func() { - BeforeAll(func() { - By("installing prometheus operator") - Expect(utils.InstallPrometheusOperator()).To(Succeed()) +var ( + ParallelOperationsForTest int32 = 2 +) - By("installing the cert-manager") - Expect(utils.InstallCertManager()).To(Succeed()) +var _ = Describe("E2E tests of maintenance operator", Ordered, func() { + var cleaupObjects []client.Object - By("creating manager namespace") - cmd := exec.Command("kubectl", "create", "ns", namespace) - _, _ = utils.Run(cmd) + BeforeAll(func() { + By("set maintenanceOperatorConfig maxParallelOperations to 2") + m := &maintenancev1.MaintenanceOperatorConfig{} + m.Name = "default" + m.Namespace = maintenanceOperatorNamespace + Expect(k8sClient.Get(testContext, client.ObjectKeyFromObject(m), m)).To(Succeed()) + m.Spec.MaxParallelOperations = ptr.To(intstr.FromInt32(ParallelOperationsForTest)) + Expect(k8sClient.Update(testContext, m)).To(Succeed()) + By("deploy test workload") + uns, err := e2eutils.LoadObjectFromFile("test_workload.yaml") + Expect(err).NotTo(HaveOccurred()) + deployment := &appsv1.Deployment{} + Expect(e2eutils.ToConcrete(uns, deployment)).To(Succeed()) + deployment.Spec.Replicas = ptr.To(int32(len(testCluster.workerNodes))) + Expect(k8sClient.Create(testContext, deployment)).To(Succeed()) + cleaupObjects = append(cleaupObjects, deployment) + By("wait for deployment pods to be running") + Eventually(func() int { + readyPods := 0 + pods := &corev1.PodList{} + Expect(k8sClient.List( + testContext, pods, client.InNamespace(deployment.Namespace), + client.MatchingLabels(deployment.Spec.Selector.MatchLabels))).To(Succeed()) + for _, p := range pods.Items { + if p.Status.Phase == corev1.PodRunning { + readyPods++ + } + } + return readyPods + }).WithTimeout(30 * time.Second).Should(Equal(len(testCluster.workerNodes))) }) AfterAll(func() { - By("uninstalling the Prometheus manager bundle") - utils.UninstallPrometheusOperator() - - By("uninstalling the cert-manager bundle") - utils.UninstallCertManager() - - By("removing manager namespace") - cmd := exec.Command("kubectl", "delete", "ns", namespace) - _, _ = utils.Run(cmd) + for _, o := range cleaupObjects { + err := k8sClient.Delete(testContext, o) + if err != nil && !k8serrors.IsNotFound(err) { + Fail(fmt.Sprintf("failed to delete object %s: %v", o.GetName(), err)) + } + } + + // wait for objects to be deleted + for _, o := range cleaupObjects { + Eventually(func() error { + return k8sClient.Get(testContext, client.ObjectKeyFromObject(o), o) + }, time.Minute, time.Second).ShouldNot(Succeed()) + } }) - Context("Operator", func() { - It("should run successfully", func() { - var controllerPodName string - var err error - - // projectimage stores the name of the image used in the example - var projectimage = "example.com/maintenance-operator:v0.0.1" - - By("building the manager(Operator) image") - cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage)) - _, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("loading the the manager(Operator) image on Kind") - err = utils.LoadImageToKindClusterWithName(projectimage) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("installing CRDs") - cmd = exec.Command("make", "install") - _, err = utils.Run(cmd) - - By("deploying the controller-manager") - cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage)) - _, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("validating that the controller-manager pod is running as expected") - verifyControllerUp := func() error { - // Get pod name - - cmd = exec.Command("kubectl", "get", - "pods", "-l", "control-plane=controller-manager", - "-o", "go-template={{ range .items }}"+ - "{{ if not .metadata.deletionTimestamp }}"+ - "{{ .metadata.name }}"+ - "{{ \"\\n\" }}{{ end }}{{ end }}", - "-n", namespace, - ) - - podOutput, err := utils.Run(cmd) - ExpectWithOffset(2, err).NotTo(HaveOccurred()) - podNames := utils.GetNonEmptyLines(string(podOutput)) - if len(podNames) != 1 { - return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames)) - } - controllerPodName = podNames[0] - ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("controller-manager")) - - // Validate pod status - cmd = exec.Command("kubectl", "get", - "pods", controllerPodName, "-o", "jsonpath={.status.phase}", - "-n", namespace, - ) - status, err := utils.Run(cmd) - ExpectWithOffset(2, err).NotTo(HaveOccurred()) - if string(status) != "Running" { - return fmt.Errorf("controller pod in %s status", status) - } - return nil + It("E2E test flow should run successfully", func() { + nmUns, err := e2eutils.LoadObjectFromFile("test_maintenance.yaml") + Expect(err).NotTo(HaveOccurred()) + nmTpl := &maintenancev1.NodeMaintenance{} + Expect(e2eutils.ToConcrete(nmUns, nmTpl)).To(Succeed()) + By("create NodeMaintenance requests for all worker nodes with two requests per node with different requestorIDs") + for _, nodeName := range testCluster.workerNodes { + nm1 := generateNMFromTemplate(nmTpl, fmt.Sprintf("%s-%s", "one", nodeName), nodeName, "one.test.com") + nm2 := generateNMFromTemplate(nmTpl, fmt.Sprintf("%s-%s", "two", nodeName), nodeName, "two.test.com") + Expect(k8sClient.Create(testContext, nm1)).To(Succeed()) + cleaupObjects = append(cleaupObjects, nm1) + Expect(k8sClient.Create(testContext, nm2)).To(Succeed()) + cleaupObjects = append(cleaupObjects, nm2) + } + + By("validate NoteMaintenance state in the cluster") + for i := 0; i < len(testCluster.workerNodes)*2; { + readyNMs := validateState(int(ParallelOperationsForTest)) + By("delete ready NodeMaintenance requests") + for _, nm := range readyNMs { + Expect(k8sClient.Delete(testContext, nm)).To(Succeed()) } - EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed()) - - }) + By("wait for ready NodeMaintenance to be deleted") + for _, nm := range readyNMs { + Eventually(func() error { + return k8sClient.Get(testContext, client.ObjectKeyFromObject(nm), nm) + }).WithTimeout(60 * time.Second).ShouldNot(Succeed()) + } + i += len(readyNMs) + } + By("ensure all NodeMaintenance requests are deleted") + Eventually(func() int { + nms := &maintenancev1.NodeMaintenanceList{} + Expect(k8sClient.List(testContext, nms)).To(Succeed()) + return len(nms.Items) + }).WithTimeout(60 * time.Second).Should(Equal(0)) }) }) + +// generateNMFromTemplate generates a new NodeMaintenance object from the template with the provided name, nodeName and requestorID +func generateNMFromTemplate(tpl *maintenancev1.NodeMaintenance, name, nodeName, requestorID string) *maintenancev1.NodeMaintenance { + nm := tpl.DeepCopy() + nm.Name = name + nm.Spec.NodeName = nodeName + nm.Spec.RequestorID = requestorID + return nm +} + +// validateState validates that we eventually have expectedReady NodeMaintenances in ready state and then ensure +// it stays that way consistently. return ready NodeMaintenances for further processing. +func validateState(expectedReady int) []*maintenancev1.NodeMaintenance { + By("wait for NodeMaintenance requests to be in ready state") + Eventually(getReadyNodeMaintenance).WithTimeout(60 * time.Second).WithPolling(1 * time.Second). + Should(HaveLen(expectedReady)) + Consistently(getReadyNodeMaintenance).WithTimeout(10 * time.Second).WithPolling(1 * time.Second). + Should(HaveLen(expectedReady)) + readyNMs := getReadyNodeMaintenance() + By("ensure ready NodeMaintenance requests are for different nodes") + nodeNames := make(map[string]struct{}) + for _, nm := range readyNMs { + if _, ok := nodeNames[nm.Spec.NodeName]; ok { + Fail(fmt.Sprintf("NodeMaintenance request for node %s is duplicated", nm.Spec.NodeName)) + } + nodeNames[nm.Spec.NodeName] = struct{}{} + } + return readyNMs +} + +// getReadyNodeMaintenance returns NodeMaintenance objects in Ready state +func getReadyNodeMaintenance() []*maintenancev1.NodeMaintenance { + nms := &maintenancev1.NodeMaintenanceList{} + ExpectWithOffset(1, k8sClient.List(testContext, nms)).To(Succeed()) + var readyNMs []*maintenancev1.NodeMaintenance + for i := range nms.Items { + if k8sutils.GetReadyConditionReason(&nms.Items[i]) == maintenancev1.ConditionReasonReady { + readyNMs = append(readyNMs, &nms.Items[i]) + } + } + GinkgoWriter.Printf("Ready NodeMaintenances: %d\n", len(readyNMs)) + return readyNMs +} diff --git a/test/utils/utils.go b/test/utils/utils.go index 57691e6..66ebe2a 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -1,17 +1,17 @@ /* -Copyright 2024. + Copyright 2024, NVIDIA CORPORATION & AFFILIATES -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 + 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 + 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. + 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 utils @@ -19,122 +19,38 @@ package utils import ( "fmt" "os" - "os/exec" - "strings" + "path/filepath" . "github.com/onsi/ginkgo/v2" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" ) -const ( - prometheusOperatorVersion = "v0.68.0" - prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + - "releases/download/%s/bundle.yaml" - - certmanagerVersion = "v1.5.3" - certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml" -) - -func warnError(err error) { - fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) -} - -// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. -func InstallPrometheusOperator() error { - url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) - cmd := exec.Command("kubectl", "create", "-f", url) - _, err := Run(cmd) - return err -} - -// Run executes the provided command within this context -func Run(cmd *exec.Cmd) ([]byte, error) { - dir, _ := GetProjectDir() - cmd.Dir = dir - - if err := os.Chdir(cmd.Dir); err != nil { - fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) - } - - cmd.Env = append(os.Environ(), "GO111MODULE=on") - command := strings.Join(cmd.Args, " ") - fmt.Fprintf(GinkgoWriter, "running: %s\n", command) - output, err := cmd.CombinedOutput() +// LoadObjectFromFile loads an object from "objects" directory with given file name +func LoadObjectFromFile(file string) (*unstructured.Unstructured, error) { + fullPath := filepath.Join("objects", file) + obj := &unstructured.Unstructured{} + b, err := os.ReadFile(fullPath) if err != nil { - return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) - } - - return output, nil -} - -// UninstallPrometheusOperator uninstalls the prometheus -func UninstallPrometheusOperator() { - url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) - cmd := exec.Command("kubectl", "delete", "-f", url) - if _, err := Run(cmd); err != nil { - warnError(err) + return nil, err } -} - -// UninstallCertManager uninstalls the cert manager -func UninstallCertManager() { - url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) - cmd := exec.Command("kubectl", "delete", "-f", url) - if _, err := Run(cmd); err != nil { - warnError(err) - } -} - -// InstallCertManager installs the cert manager bundle. -func InstallCertManager() error { - url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) - cmd := exec.Command("kubectl", "apply", "-f", url) - if _, err := Run(cmd); err != nil { - return err - } - // Wait for cert-manager-webhook to be ready, which can take time if cert-manager - // was re-installed after uninstalling on a cluster. - cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", - "--for", "condition=Available", - "--namespace", "cert-manager", - "--timeout", "5m", - ) - - _, err := Run(cmd) - return err -} - -// LoadImageToKindClusterWithName loads a local docker image to the kind cluster -func LoadImageToKindClusterWithName(name string) error { - cluster := "kind" - if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { - cluster = v + b, err = yaml.YAMLToJSON(b) + if err != nil { + return nil, err } - kindOptions := []string{"load", "docker-image", name, "--name", cluster} - cmd := exec.Command("kind", kindOptions...) - _, err := Run(cmd) - return err -} - -// GetNonEmptyLines converts given command output string into individual objects -// according to line breakers, and ignores the empty elements in it. -func GetNonEmptyLines(output string) []string { - var res []string - elements := strings.Split(output, "\n") - for _, element := range elements { - if element != "" { - res = append(res, element) - } + if err = obj.UnmarshalJSON(b); err != nil { + return nil, err } - - return res + return obj, nil } -// GetProjectDir will return the directory where the project is -func GetProjectDir() (string, error) { - wd, err := os.Getwd() +// ToConcrete converts an unstructured object to a concrete object +func ToConcrete[T client.Object](obj *unstructured.Unstructured, concrete T) error { + err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, concrete) if err != nil { - return wd, err + return fmt.Errorf("error converting unstructured to %T: %v", concrete, err) } - wd = strings.ReplaceAll(wd, "/test/e2e", "") - return wd, nil + return nil }