diff --git a/tests/e2e/README.md b/tests/e2e/README.md index f84344c43..c95651140 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -74,6 +74,23 @@ For example, run only "Complex text" without cleanup on failure: FOCUS="Complex test" STOP_ON_FAILURE=yes task run ``` +### Reusable mode option + +The environment variable REUSABLE used to retain all resources created during e2e test after its completion (no cleanup). +When a test starts, it will reuse existing virtual machines created earlier, if they exist. +If no virtual machines were found, they will be created. + +For example, run test in reusable mode: +```bash +REUSABLE=yes task run +``` + +! Only the following e2e tests are supported in REUSABLE mode. All other tests will be skipped. +- "Virtual machine configuration" +- "Virtual machine migration" +- "VM connectivity" +- "Complex test" + ## Run tests in CI ```bash task run:ci diff --git a/tests/e2e/affinity_toleration_test.go b/tests/e2e/affinity_toleration_test.go index fd8736321..798736502 100644 --- a/tests/e2e/affinity_toleration_test.go +++ b/tests/e2e/affinity_toleration_test.go @@ -24,11 +24,18 @@ import ( v1 "k8s.io/api/core/v1" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" kc "github.com/deckhouse/virtualization/tests/e2e/kubectl" ) var _ = Describe("Virtual machine affinity and toleration", ginkgoutil.CommonE2ETestDecorators(), func() { + BeforeEach(func() { + if config.IsReusable() { + Skip("Test not available in REUSABLE mode: not supported yet.") + } + }) + var ( testCaseLabel = map[string]string{"testcase": "affinity-toleration"} vmA = map[string]string{"vm": "vm-a"} @@ -37,6 +44,15 @@ var _ = Describe("Virtual machine affinity and toleration", ginkgoutil.CommonE2E vmD = map[string]string{"vm": "vm-d"} ) + Context("Preparing the environment", func() { + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.AffinityToleration, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) + }) + Context("When virtualization resources are applied:", func() { It("result should be succeeded", func() { res := kubectl.Apply(kc.ApplyOptions{ diff --git a/tests/e2e/complex_test.go b/tests/e2e/complex_test.go index 8792b0783..07bfee4f5 100644 --- a/tests/e2e/complex_test.go +++ b/tests/e2e/complex_test.go @@ -24,6 +24,7 @@ import ( . "github.com/onsi/gomega" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" kc "github.com/deckhouse/virtualization/tests/e2e/kubectl" ) @@ -65,8 +66,30 @@ var _ = Describe("Complex test", ginkgoutil.CommonE2ETestDecorators(), func() { hasNoConsumerLabel = map[string]string{"hasNoConsumer": "complex-test"} ) + Context("Preparing the environment", func() { + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.ComplexTest, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) + }) + Context("When virtualization resources are applied", func() { It("result should be succeeded", func() { + if config.IsReusable() { + res := kubectl.List(kc.ResourceVM, kc.GetOptions{ + Labels: testCaseLabel, + Namespace: conf.Namespace, + Output: "jsonpath='{.items[*].metadata.name}'", + }) + Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) + + if res.StdOut() != "" { + return + } + } + res := kubectl.Apply(kc.ApplyOptions{ Filename: []string{conf.TestData.ComplexTest}, FilenameOption: kc.Kustomize, @@ -231,15 +254,20 @@ var _ = Describe("Complex test", ginkgoutil.CommonE2ETestDecorators(), func() { Context("When test is completed", func() { It("deletes test case resources", func() { - DeleteTestCaseResources(ResourcesToDelete{ - KustomizationDir: conf.TestData.ComplexTest, + resourcesToDelete := ResourcesToDelete{ AdditionalResources: []AdditionalResource{ { kc.ResourceKubevirtVMIM, testCaseLabel, }, }, - }) + } + + if !config.IsReusable() { + resourcesToDelete.KustomizationDir = conf.TestData.ComplexTest + } + + DeleteTestCaseResources(resourcesToDelete) }) }) }) diff --git a/tests/e2e/config/config.go b/tests/e2e/config/config.go index 4c8cb4cb9..d633adcc4 100644 --- a/tests/e2e/config/config.go +++ b/tests/e2e/config/config.go @@ -17,9 +17,13 @@ limitations under the License. package config import ( + "errors" "fmt" "log" "os" + "path/filepath" + "reflect" + "slices" "strconv" yamlv3 "gopkg.in/yaml.v3" @@ -31,12 +35,12 @@ import ( var ( conf *Config - err error git gt.Git kubectl kc.Kubectl ) func init() { + var err error if conf, err = GetConfig(); err != nil { log.Fatal(err) } @@ -221,6 +225,26 @@ func (c *Config) setEnvs() error { return nil } +func (c *Config) GetTestCases() ([]string, error) { + testDataValue := reflect.ValueOf(c.TestData) + testDataType := reflect.TypeOf(c.TestData) + excludedData := []string{"Sshkey", "SshUser"} + testCases := make([]string, 0, testDataType.NumField()-len(excludedData)) + + if testDataType.Kind() == reflect.Struct { + for i := 0; i < testDataType.NumField(); i++ { + field := testDataType.Field(i) + value := testDataValue.Field(i) + if !slices.Contains(excludedData, field.Name) { + testCases = append(testCases, fmt.Sprintf("%v", value.Interface())) + } + } + return testCases, nil + } else { + return nil, errors.New("`config.TestData` it is not a structure") + } +} + func GetNamePrefix() (string, error) { if prNumber, ok := os.LookupEnv("MODULES_MODULE_TAG"); ok && prNumber != "" { return prNumber, nil @@ -228,7 +252,7 @@ func GetNamePrefix() (string, error) { res := git.GetHeadHash() if !res.WasSuccess() { - return "", fmt.Errorf(res.StdErr()) + return "", errors.New(res.StdErr()) } commitHash := res.StdOut() @@ -237,6 +261,10 @@ func GetNamePrefix() (string, error) { return commitHash, nil } +func (c *Config) SetNamespace(name string) { + c.Namespace = name +} + func (k *Kustomize) SetParams(filePath, namespace, namePrefix string) error { var kustomizeFile Kustomize @@ -250,7 +278,10 @@ func (k *Kustomize) SetParams(filePath, namespace, namePrefix string) error { return unmarshalErr } - kustomizeFile.Namespace = namespace + fileDir := filepath.Dir(filePath) + testCaseName := filepath.Base(fileDir) + + kustomizeFile.Namespace = namespace + "-" + testCaseName kustomizeFile.NamePrefix = namePrefix + "-" kustomizeFile.Labels[0].Pairs["id"] = namePrefix updatedKustomizeFile, marshalErr := yamlv3.Marshal(&kustomizeFile) @@ -266,6 +297,22 @@ func (k *Kustomize) SetParams(filePath, namespace, namePrefix string) error { return nil } +func (k *Kustomize) GetNamespace(filePath string) (string, error) { + var kustomizeFile Kustomize + + data, readErr := os.ReadFile(filePath) + if readErr != nil { + return "", fmt.Errorf("cannot get namespace from %s: %w", filePath, readErr) + } + + unmarshalErr := yamlv3.Unmarshal([]byte(data), &kustomizeFile) + if unmarshalErr != nil { + return "", fmt.Errorf("cannot get namespace from %s: %w", filePath, unmarshalErr) + } + + return kustomizeFile.Namespace, nil +} + func (k *Kustomize) ExcludeResource(filePath, resourceName string) error { var kustomizeFile Kustomize @@ -302,7 +349,7 @@ func (k *Kustomize) ExcludeResource(filePath, resourceName string) error { func GetModuleConfig() (*ModuleConfig, error) { res := kubectl.GetResource(kc.ResourceModuleConfig, "virtualization", kc.GetOptions{Output: "yaml"}) if !res.WasSuccess() { - return nil, fmt.Errorf(res.StdErr()) + return nil, errors.New(res.StdErr()) } var mc ModuleConfig diff --git a/tests/e2e/config/reusable.go b/tests/e2e/config/reusable.go new file mode 100644 index 000000000..f5626befc --- /dev/null +++ b/tests/e2e/config/reusable.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 Flant JSC + +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 config + +import ( + "fmt" + "os" +) + +// ReusableEnv defines an environment variable used to retain all resources created during e2e test after its completion (no cleanup). +// When a test starts, it will reuse existing virtual machines created earlier, if they exist. +// If no virtual machines were found, they will be created. +// Only the following e2e tests are supported in REUSABLE mode. All other tests will be skipped. +// - "Virtual machine configuration" +// - "Virtual machine migration" +// - "VM connectivity" +// - "Complex test" +const ReusableEnv = "REUSABLE" + +const reusableValue = "yes" + +func CheckReusableOption() error { + env := os.Getenv(ReusableEnv) + switch env { + case reusableValue, "": + return nil + default: + return fmt.Errorf("invalid value for the REUSABLE env: %q", env) + } +} + +func IsReusable() bool { + return os.Getenv(ReusableEnv) == reusableValue +} diff --git a/tests/e2e/sizing_policy_test.go b/tests/e2e/sizing_policy_test.go index eae1be27b..0bc14bb0f 100644 --- a/tests/e2e/sizing_policy_test.go +++ b/tests/e2e/sizing_policy_test.go @@ -26,6 +26,7 @@ import ( . "github.com/onsi/gomega" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" . "github.com/deckhouse/virtualization/tests/e2e/helper" kc "github.com/deckhouse/virtualization/tests/e2e/kubectl" @@ -67,6 +68,12 @@ func CompareVirtualMachineClassReadyStatus(vmName, expectedStatus string) { } var _ = Describe("Sizing policy", ginkgoutil.CommonE2ETestDecorators(), func() { + BeforeEach(func() { + if config.IsReusable() { + Skip("Test not available in REUSABLE mode: not supported yet.") + } + }) + var ( vmNotValidSizingPolicyChanging string vmNotValidSizingPolicyCreating string @@ -85,6 +92,13 @@ var _ = Describe("Sizing policy", ginkgoutil.CommonE2ETestDecorators(), func() { vmClassDiscovery = fmt.Sprintf("%s-discovery", namePrefix) vmClassDiscoveryCopy = fmt.Sprintf("%s-discovery-copy", namePrefix) newVmClassFilePath = fmt.Sprintf("%s/vmc-copy.yaml", conf.TestData.SizingPolicy) + + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.SizingPolicy, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) }) Context("When resources are applied", func() { diff --git a/tests/e2e/tests_suite_test.go b/tests/e2e/tests_suite_test.go index 88ab8b608..c848f5040 100644 --- a/tests/e2e/tests_suite_test.go +++ b/tests/e2e/tests_suite_test.go @@ -64,7 +64,11 @@ var ( ) func init() { - var err error + err := config.CheckReusableOption() + if err != nil { + log.Println("To run tests in REUSABLE mode, set REUSABLE=yes. If you don't intend to use this mode, leave the variable unset.") + log.Fatal(err) + } if conf, err = config.GetConfig(); err != nil { log.Fatal(err) } @@ -108,13 +112,14 @@ func init() { log.Fatal(err) } } - err = Cleanup() - if err != nil { - log.Fatal(err) - } - res := kubectl.CreateResource(kc.ResourceNamespace, conf.Namespace, kc.CreateOptions{}) - if !res.WasSuccess() { - log.Fatalf("err: %v\n%s", res.Error(), res.StdErr()) + + if !config.IsReusable() { + err := Cleanup() + if len(err) != 0 { + log.Fatal(err) + } + } else { + log.Println("Run test in REUSABLE mode") } } @@ -122,33 +127,65 @@ func TestTests(t *testing.T) { RegisterFailHandler(Fail) fmt.Fprintf(GinkgoWriter, "Starting test suite\n") RunSpecs(t, "Tests") - if !(ginkgoutil.FailureBehaviourEnvSwitcher{}).IsStopOnFailure() { - Cleanup() + + if (ginkgoutil.FailureBehaviourEnvSwitcher{}).IsStopOnFailure() || config.IsReusable() { + return + } + + err := Cleanup() + if len(err) != 0 { + log.Fatal(err) } } -func Cleanup() error { - res := kubectl.Delete(kc.DeleteOptions{ - Filename: []string{conf.Namespace}, - IgnoreNotFound: true, - Resource: kc.ResourceNamespace, - }) - if res.Error() != nil { - return fmt.Errorf("cmd: %s\nstderr: %s", res.GetCmd(), res.StdErr()) +func Cleanup() []error { + cleanupErrs := make([]error, 0) + testCases, err := conf.GetTestCases() + if err != nil { + cleanupErrs = append(cleanupErrs, err) + return cleanupErrs } - res = kubectl.Delete(kc.DeleteOptions{ + + for _, tc := range testCases { + kustomizeFilePath := fmt.Sprintf("%s/kustomization.yaml", tc) + namespace, err := kustomize.GetNamespace(kustomizeFilePath) + if err != nil { + cleanupErrs = append( + cleanupErrs, fmt.Errorf("cannot cleanup namespace %q: %w", namespace, err), + ) + continue + } + res := kubectl.Delete(kc.DeleteOptions{ + Filename: []string{conf.Namespace}, + IgnoreNotFound: true, + Resource: kc.ResourceNamespace, + }) + if res.Error() != nil { + cleanupErrs = append( + cleanupErrs, fmt.Errorf("cmd: %s\nstderr: %s", res.GetCmd(), res.StdErr()), + ) + continue + } + } + + res := kubectl.Delete(kc.DeleteOptions{ Labels: map[string]string{"id": namePrefix}, Resource: kc.ResourceCVI, }) if res.Error() != nil { - return fmt.Errorf("cmd: %s\nstderr: %s", res.GetCmd(), res.StdErr()) + cleanupErrs = append( + cleanupErrs, fmt.Errorf("cmd: %s\nstderr: %s", res.GetCmd(), res.StdErr()), + ) } res = kubectl.Delete(kc.DeleteOptions{ Labels: map[string]string{"id": namePrefix}, Resource: kc.ResourceVMClass, }) if res.Error() != nil { - return fmt.Errorf("cmd: %s\nstderr: %s", res.GetCmd(), res.StdErr()) + cleanupErrs = append( + cleanupErrs, fmt.Errorf("cmd: %s\nstderr: %s", res.GetCmd(), res.StdErr()), + ) } - return nil + + return cleanupErrs } diff --git a/tests/e2e/util_test.go b/tests/e2e/util_test.go index f8313f43b..415af6348 100644 --- a/tests/e2e/util_test.go +++ b/tests/e2e/util_test.go @@ -454,10 +454,7 @@ type ResourcesToDelete struct { // This function checks that all resources in test case can be deleted correctly. func DeleteTestCaseResources(resources ResourcesToDelete) { By("Response on deletion request should be successful") - errMessage := "cannot delete test case resources" - kustimizationFile := fmt.Sprintf("%s/%s", resources.KustomizationDir, "kustomization.yaml") - err := kustomize.ExcludeResource(kustimizationFile, "ns.yaml") - Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("%s\nkustomizationDir: %s\nstderr: %s", errMessage, resources.KustomizationDir, err)) + const errMessage = "cannot delete test case resources" if resources.KustomizationDir != "" { res := kubectl.Delete(kc.DeleteOptions{ diff --git a/tests/e2e/vd_snapshots_test.go b/tests/e2e/vd_snapshots_test.go index 6fef69136..ac22fd3d1 100644 --- a/tests/e2e/vd_snapshots_test.go +++ b/tests/e2e/vd_snapshots_test.go @@ -23,14 +23,16 @@ import ( "sync" "time" - sdsrepvolv1 "github.com/deckhouse/sds-replicated-volume/api/v1alpha1" snapshotvolv1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" storagev1 "k8s.io/api/storage/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + sdsrepvolv1 "github.com/deckhouse/sds-replicated-volume/api/v1alpha1" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" . "github.com/deckhouse/virtualization/tests/e2e/helper" kc "github.com/deckhouse/virtualization/tests/e2e/kubectl" @@ -226,6 +228,12 @@ func CheckFilesystemReadyStatus(vmName string, status v1.ConditionStatus) (strin } var _ = Describe("Virtual disk snapshots", ginkgoutil.CommonE2ETestDecorators(), func() { + BeforeEach(func() { + if config.IsReusable() { + Skip("Test not available in REUSABLE mode: not supported yet.") + } + }) + var ( immediateStorageClassName string // require for unattached virtual disk snapshots defaultVolumeSnapshotClassName string @@ -235,7 +243,14 @@ var _ = Describe("Virtual disk snapshots", ginkgoutil.CommonE2ETestDecorators(), vmAutomaticWithHotplug = map[string]string{"vm": "automatic-with-hotplug"} ) - Context("Environment preparing", func() { + Context("Preparing the environment", func() { + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.VdSnapshots, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) + It("prepares `Immediate` storage class and virtual disk that use it", func() { sc, err := GetDefaultStorageClass() Expect(err).NotTo(HaveOccurred(), "cannot get default storage class\nstderr: %s", err) diff --git a/tests/e2e/vm_configuration_test.go b/tests/e2e/vm_configuration_test.go index fad65bd4d..4ae7e73c0 100644 --- a/tests/e2e/vm_configuration_test.go +++ b/tests/e2e/vm_configuration_test.go @@ -18,12 +18,14 @@ package e2e import ( "fmt" + "strconv" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" d8 "github.com/deckhouse/virtualization/tests/e2e/d8" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" kc "github.com/deckhouse/virtualization/tests/e2e/kubectl" @@ -51,7 +53,7 @@ func ExecSshCommand(vmName, cmd string) { }).WithTimeout(Timeout).WithPolling(Interval).ShouldNot(HaveOccurred()) } -func ChangeCPUCoresNumber(namespace string, cpuNumber int, virtualMachines ...string) { +func ChangeCPUCoresNumber(cpuNumber int, virtualMachines ...string) { vms := strings.Join(virtualMachines, " ") cmd := fmt.Sprintf("patch %s --namespace %s %s --type merge --patch '{\"spec\":{\"cpu\":{\"cores\":%d}}}'", kc.ResourceVM, conf.Namespace, vms, cpuNumber) By("Patching virtual machine specification") @@ -90,8 +92,30 @@ var _ = Describe("Virtual machine configuration", ginkgoutil.CommonE2ETestDecora manualLabel = map[string]string{"vm": "manual-conf"} ) + Context("Preparing the environment", func() { + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.VmConfiguration, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) + }) + Context("When resources are applied", func() { It("result should be succeeded", func() { + if config.IsReusable() { + res := kubectl.List(kc.ResourceVM, kc.GetOptions{ + Labels: testCaseLabel, + Namespace: conf.Namespace, + Output: "jsonpath='{.items[*].metadata.name}'", + }) + Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) + + if res.StdOut() != "" { + return + } + } + res := kubectl.Apply(kc.ApplyOptions{ Filename: []string{conf.TestData.VmConfiguration}, FilenameOption: kc.Kustomize, @@ -132,6 +156,9 @@ var _ = Describe("Virtual machine configuration", ginkgoutil.CommonE2ETestDecora }) Describe("Manual restart approval mode", func() { + var oldCpuCores int + var newCPUCores int + Context(fmt.Sprintf("When virtual machine is in %s phase", PhaseRunning), func() { It("changes the number of processor cores", func() { res := kubectl.List(kc.ResourceVM, kc.GetOptions{ @@ -142,8 +169,17 @@ var _ = Describe("Virtual machine configuration", ginkgoutil.CommonE2ETestDecora Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) vms := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumber(ManualMode, StageBefore, 1, vms...) - ChangeCPUCoresNumber(conf.Namespace, 2, vms...) + Expect(vms).NotTo(BeEmpty()) + + vmResource := virtv2.VirtualMachine{} + err := GetObject(kc.ResourceVM, vms[0], &vmResource, kc.GetOptions{Namespace: conf.Namespace}) + Expect(err).NotTo(HaveOccurred(), err) + + oldCpuCores = vmResource.Spec.CPU.Cores + newCPUCores = 1 + (vmResource.Spec.CPU.Cores & 1) + + CheckCPUCoresNumber(ManualMode, StageBefore, oldCpuCores, vms...) + ChangeCPUCoresNumber(newCPUCores, vms...) }) }) @@ -157,7 +193,7 @@ var _ = Describe("Virtual machine configuration", ginkgoutil.CommonE2ETestDecora Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) vms := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumber(ManualMode, StageAfter, 2, vms...) + CheckCPUCoresNumber(ManualMode, StageAfter, newCPUCores, vms...) }) }) @@ -193,12 +229,15 @@ var _ = Describe("Virtual machine configuration", ginkgoutil.CommonE2ETestDecora Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) vms := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumberFromVirtualMachine("2", vms...) + CheckCPUCoresNumberFromVirtualMachine(strconv.FormatInt(int64(newCPUCores), 10), vms...) }) }) }) Describe("Automatic restart approval mode", func() { + var oldCpuCores int + var newCPUCores int + Context(fmt.Sprintf("When virtual machine is in %s phase", PhaseRunning), func() { It("changes the number of processor cores", func() { res := kubectl.List(kc.ResourceVM, kc.GetOptions{ @@ -209,8 +248,17 @@ var _ = Describe("Virtual machine configuration", ginkgoutil.CommonE2ETestDecora Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) vms := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumber(AutomaticMode, StageBefore, 1, vms...) - ChangeCPUCoresNumber(conf.Namespace, 2, vms...) + Expect(vms).NotTo(BeEmpty()) + + vmResource := virtv2.VirtualMachine{} + err := GetObject(kc.ResourceVM, vms[0], &vmResource, kc.GetOptions{Namespace: conf.Namespace}) + Expect(err).NotTo(HaveOccurred(), err) + + oldCpuCores = vmResource.Spec.CPU.Cores + newCPUCores = 1 + (vmResource.Spec.CPU.Cores & 1) + + CheckCPUCoresNumber(AutomaticMode, StageBefore, oldCpuCores, vms...) + ChangeCPUCoresNumber(newCPUCores, vms...) }) }) @@ -224,7 +272,7 @@ var _ = Describe("Virtual machine configuration", ginkgoutil.CommonE2ETestDecora Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) vms := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumber(AutomaticMode, StageAfter, 2, vms...) + CheckCPUCoresNumber(AutomaticMode, StageAfter, newCPUCores, vms...) }) }) @@ -248,16 +296,20 @@ var _ = Describe("Virtual machine configuration", ginkgoutil.CommonE2ETestDecora Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) vms := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumberFromVirtualMachine("2", vms...) + CheckCPUCoresNumberFromVirtualMachine(strconv.FormatInt(int64(newCPUCores), 10), vms...) }) }) }) Context("When test is completed", func() { It("deletes test case resources", func() { - DeleteTestCaseResources(ResourcesToDelete{ - KustomizationDir: conf.TestData.VmConfiguration, - }) + var resourcesToDelete ResourcesToDelete + + if !config.IsReusable() { + resourcesToDelete.KustomizationDir = conf.TestData.VmConfiguration + } + + DeleteTestCaseResources(resourcesToDelete) }) }) }) diff --git a/tests/e2e/vm_connectivity_test.go b/tests/e2e/vm_connectivity_test.go index 8ad190982..f9fe0724a 100644 --- a/tests/e2e/vm_connectivity_test.go +++ b/tests/e2e/vm_connectivity_test.go @@ -26,6 +26,7 @@ import ( corev1 "k8s.io/api/core/v1" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" d8 "github.com/deckhouse/virtualization/tests/e2e/d8" "github.com/deckhouse/virtualization/tests/e2e/executor" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" @@ -100,10 +101,35 @@ var _ = Describe("VM connectivity", ginkgoutil.CommonE2ETestDecorators(), func() vmA, vmB virtv2.VirtualMachine svcA, svcB corev1.Service err error + + selectorA string + selectorB string ) + Context("Preparing the environment", func() { + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.Connectivity, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) + }) + Context("When resources are applied", func() { It("result should be succeeded", func() { + if config.IsReusable() { + res := kubectl.List(kc.ResourceVM, kc.GetOptions{ + Labels: testCaseLabel, + Namespace: conf.Namespace, + Output: "jsonpath='{.items[*].metadata.name}'", + }) + Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) + + if res.StdOut() != "" { + return + } + } + res := kubectl.Apply(kc.ApplyOptions{ Filename: []string{conf.TestData.Connectivity}, FilenameOption: kc.Kustomize, @@ -226,18 +252,20 @@ var _ = Describe("VM connectivity", ginkgoutil.CommonE2ETestDecorators(), func() }) It(fmt.Sprintf("changes selector in service %s with selector from service %s", aObjName, bObjName), func() { + selectorA = svcA.Spec.Selector["service"] + selectorB = svcB.Spec.Selector["service"] + PatchResource(kc.ResourceService, svcA.Name, &kc.JsonPatch{ Op: "replace", Path: "/spec/selector/service", - Value: svcB.Spec.Selector["service"], + Value: selectorB, }) }) It(fmt.Sprintf("checks selector in service %s", aObjName), func() { - By(fmt.Sprintf("Selector should be %q", svcB.Spec.Selector["service"])) - label := svcB.Spec.Selector["service"] + By(fmt.Sprintf("Selector should be %q", selectorB)) output := "jsonpath={.spec.selector.service}" - CheckField(kc.ResourceService, svcA.Name, output, label) + CheckField(kc.ResourceService, svcA.Name, output, selectorB) }) It(fmt.Sprintf("gets page from service %s", aObjName), func() { @@ -251,19 +279,38 @@ var _ = Describe("VM connectivity", ginkgoutil.CommonE2ETestDecorators(), func() return strings.TrimSpace(res.StdOut()), nil }).WithTimeout(Timeout).WithPolling(Interval).Should(ContainSubstring(vmB.Name)) }) + + It(fmt.Sprintf("changes back selector in service %s", aObjName), func() { + PatchResource(kc.ResourceService, svcA.Name, &kc.JsonPatch{ + Op: "replace", + Path: "/spec/selector/service", + Value: selectorA, + }) + }) + + It(fmt.Sprintf("checks selector in service %s", aObjName), func() { + By(fmt.Sprintf("Selector should be %q", selectorA)) + output := "jsonpath={.spec.selector.service}" + CheckField(kc.ResourceService, svcA.Name, output, selectorA) + }) }) Context("When test is completed", func() { It("deletes test case resources", func() { - DeleteTestCaseResources(ResourcesToDelete{ - KustomizationDir: conf.TestData.Connectivity, + resourcesToDelete := ResourcesToDelete{ AdditionalResources: []AdditionalResource{ { Resource: kc.ResourcePod, Labels: map[string]string{"name": CurlPod}, }, }, - }) + } + + if !config.IsReusable() { + resourcesToDelete.KustomizationDir = conf.TestData.Connectivity + } + + DeleteTestCaseResources(resourcesToDelete) }) }) }) diff --git a/tests/e2e/vm_disk_attachment_test.go b/tests/e2e/vm_disk_attachment_test.go index 5e7a32914..cf4f65b02 100644 --- a/tests/e2e/vm_disk_attachment_test.go +++ b/tests/e2e/vm_disk_attachment_test.go @@ -25,6 +25,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" d8 "github.com/deckhouse/virtualization/tests/e2e/d8" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" . "github.com/deckhouse/virtualization/tests/e2e/helper" @@ -105,6 +106,12 @@ func GetDisksMetadata(vmName string, disks *Disks) error { } var _ = Describe("Virtual disk attachment", ginkgoutil.CommonE2ETestDecorators(), func() { + BeforeEach(func() { + if config.IsReusable() { + Skip("Test not available in REUSABLE mode: not supported yet.") + } + }) + var ( testCaseLabel = map[string]string{"testcase": "vm-disk-attachment"} hasNoConsumerLabel = map[string]string{"hasNoConsumer": "vm-disk-attachment"} @@ -120,6 +127,13 @@ var _ = Describe("Virtual disk attachment", ginkgoutil.CommonE2ETestDecorators() vdAttach = fmt.Sprintf("%s-vd-attach-%s", namePrefix, nameSuffix) vmName = fmt.Sprintf("%s-vm-%s", namePrefix, nameSuffix) vmbdaName = fmt.Sprintf("%s-vm-%s", namePrefix, nameSuffix) + + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.VmDiskAttachment, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) }) Context("When resources are applied", func() { diff --git a/tests/e2e/vm_disk_resizing_test.go b/tests/e2e/vm_disk_resizing_test.go index faae2dc24..9890b8fb5 100644 --- a/tests/e2e/vm_disk_resizing_test.go +++ b/tests/e2e/vm_disk_resizing_test.go @@ -18,6 +18,7 @@ package e2e import ( "encoding/json" + "errors" "fmt" "strings" @@ -26,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" cfg "github.com/deckhouse/virtualization/tests/e2e/config" d8 "github.com/deckhouse/virtualization/tests/e2e/d8" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" @@ -97,7 +99,7 @@ func GetSizeByLsblk(vmName, diskId string) (*resource.Quantity, error) { IdenityFile: conf.TestData.Sshkey, }) if res.Error() != nil { - return nil, fmt.Errorf(res.StdErr()) + return nil, errors.New(res.StdErr()) } err := json.Unmarshal(res.StdOutBytes(), &blockDevice) if err != nil { @@ -159,8 +161,23 @@ func GetVirtualMachineDisks(vmName string, config *cfg.Config) (VirtualMachineDi } var _ = Describe("Virtual disk resizing", ginkgoutil.CommonE2ETestDecorators(), func() { + BeforeEach(func() { + if config.IsReusable() { + Skip("Test not available in REUSABLE mode: not supported yet.") + } + }) + testCaseLabel := map[string]string{"testcase": "disk-resizing"} + Context("Preparing the environment", func() { + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.DiskResizing, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) + }) + Context("When resources are applied", func() { It("result should be succeeded", func() { res := kubectl.Apply(kc.ApplyOptions{ diff --git a/tests/e2e/vm_label_annotation_test.go b/tests/e2e/vm_label_annotation_test.go index e52e3087c..f57f7d890 100644 --- a/tests/e2e/vm_label_annotation_test.go +++ b/tests/e2e/vm_label_annotation_test.go @@ -25,6 +25,7 @@ import ( v1 "k8s.io/api/core/v1" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/tests/e2e/config" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" kc "github.com/deckhouse/virtualization/tests/e2e/kubectl" ) @@ -99,9 +100,24 @@ func GetActiveVirtualMachinePod(vmObj *virtv2.VirtualMachine) string { } var _ = Describe("Virtual machine label and annotation", ginkgoutil.CommonE2ETestDecorators(), func() { + BeforeEach(func() { + if config.IsReusable() { + Skip("Test not available in REUSABLE mode: not supported yet.") + } + }) + testCaseLabel := map[string]string{"testcase": "vm-label-annotation"} specialKeyValue := map[string]string{"specialKey": "specialValue"} + Context("Preparing the environment", func() { + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.VmLabelAnnotation, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) + }) + Context("When resources are applied", func() { It("result should be succeeded", func() { res := kubectl.Apply(kc.ApplyOptions{ diff --git a/tests/e2e/vm_migration_test.go b/tests/e2e/vm_migration_test.go index b3887162c..bfca2fe92 100644 --- a/tests/e2e/vm_migration_test.go +++ b/tests/e2e/vm_migration_test.go @@ -25,6 +25,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" virtv1 "kubevirt.io/api/core/v1" + "github.com/deckhouse/virtualization/tests/e2e/config" "github.com/deckhouse/virtualization/tests/e2e/ginkgoutil" . "github.com/deckhouse/virtualization/tests/e2e/helper" kc "github.com/deckhouse/virtualization/tests/e2e/kubectl" @@ -77,8 +78,30 @@ func CreateMigrationManifest(vmName, filePath string, labels map[string]string) var _ = Describe("Virtual machine migration", ginkgoutil.CommonE2ETestDecorators(), func() { testCaseLabel := map[string]string{"testcase": "vm-migration"} + Context("Preparing the environment", func() { + It("sets the namespace", func() { + kustomization := fmt.Sprintf("%s/%s", conf.TestData.VmMigration, "kustomization.yaml") + ns, err := kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + conf.SetNamespace(ns) + }) + }) + Context("When resources are applied", func() { It("result should be succeeded", func() { + if config.IsReusable() { + res := kubectl.List(kc.ResourceVM, kc.GetOptions{ + Labels: testCaseLabel, + Namespace: conf.Namespace, + Output: "jsonpath='{.items[*].metadata.name}'", + }) + Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) + + if res.StdOut() != "" { + return + } + } + res := kubectl.Apply(kc.ApplyOptions{ Filename: []string{conf.TestData.VmMigration}, FilenameOption: kc.Kustomize, @@ -165,15 +188,20 @@ var _ = Describe("Virtual machine migration", ginkgoutil.CommonE2ETestDecorators Context("When test is completed", func() { It("deletes test case resources", func() { - DeleteTestCaseResources(ResourcesToDelete{ - KustomizationDir: conf.TestData.VmMigration, + resourcesToDelete := ResourcesToDelete{ AdditionalResources: []AdditionalResource{ { Resource: kc.ResourceKubevirtVMIM, Labels: testCaseLabel, }, }, - }) + } + + if !config.IsReusable() { + resourcesToDelete.KustomizationDir = conf.TestData.VmMigration + } + + DeleteTestCaseResources(resourcesToDelete) }) }) })