-
Notifications
You must be signed in to change notification settings - Fork 84
CI: Add a minimal virtual machine startup/teardown test. #1330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b95c281
8298ded
0b30eea
6e66cd8
d67a4a5
708e39a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ import ( | |
| operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" | ||
|
|
||
| corev1 "k8s.io/api/core/v1" | ||
| apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
|
|
@@ -23,6 +24,11 @@ import ( | |
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| ) | ||
|
|
||
| const ( | ||
| emulationAnnotation = "kubevirt.kubevirt.io/jsonpatch" | ||
| useEmulation = `[{"op": "add", "path": "/spec/configuration/developerConfiguration", "value": {"useEmulation": true}}]` | ||
| ) | ||
|
|
||
| var packageManifestsGvr = schema.GroupVersionResource{ | ||
| Group: "packages.operators.coreos.com", | ||
| Resource: "packagemanifests", | ||
|
|
@@ -35,6 +41,12 @@ var hyperConvergedGvr = schema.GroupVersionResource{ | |
| Version: "v1beta1", | ||
| } | ||
|
|
||
| var virtualMachineGvr = schema.GroupVersionResource{ | ||
| Group: "kubevirt.io", | ||
| Resource: "virtualmachines", | ||
| Version: "v1", | ||
| } | ||
|
|
||
| var csvGvr = schema.GroupVersionResource{ | ||
| Group: "operators.coreos.com", | ||
| Resource: "clusterserviceversion", | ||
|
|
@@ -210,6 +222,32 @@ func (v *VirtOperator) checkHco() bool { | |
| return health == "healthy" | ||
| } | ||
|
|
||
| // Check if KVM emulation is enabled. | ||
| func (v *VirtOperator) checkEmulation() bool { | ||
| hco, err := v.Dynamic.Resource(hyperConvergedGvr).Namespace("openshift-cnv").Get(context.Background(), "kubevirt-hyperconverged", v1.GetOptions{}) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| if hco == nil { | ||
| return false | ||
| } | ||
|
|
||
| // Look for JSON patcher annotation that enables emulation. | ||
| patcher, ok, err := unstructured.NestedString(hco.UnstructuredContent(), "metadata", "annotations", emulationAnnotation) | ||
| if err != nil { | ||
| log.Printf("Failed to get KVM emulation annotation from HCO: %v", err) | ||
| return false | ||
| } | ||
| if !ok { | ||
| log.Printf("No KVM emulation annotation (%s) listed on HCO!", emulationAnnotation) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should return false here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will as it will hit line no 248. |
||
| } | ||
| if strings.Compare(patcher, useEmulation) == 0 { | ||
| return true | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // Creates the target virtualization namespace, likely openshift-cnv or kubevirt-hyperconverged | ||
| func (v *VirtOperator) installNamespace() error { | ||
| err := v.Client.Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: v.Namespace}}) | ||
|
|
@@ -281,6 +319,36 @@ func (v *VirtOperator) installHco() error { | |
| return nil | ||
| } | ||
|
|
||
| func (v *VirtOperator) configureEmulation() error { | ||
| hco, err := v.Dynamic.Resource(hyperConvergedGvr).Namespace("openshift-cnv").Get(context.Background(), "kubevirt-hyperconverged", v1.GetOptions{}) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if hco == nil { | ||
| return fmt.Errorf("could not find hyperconverged operator to set emulation annotation") | ||
| } | ||
|
|
||
| annotations, ok, err := unstructured.NestedMap(hco.UnstructuredContent(), "metadata", "annotations") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !ok { | ||
| annotations = make(map[string]interface{}) | ||
| } | ||
| annotations[emulationAnnotation] = useEmulation | ||
|
|
||
| if err := unstructured.SetNestedMap(hco.UnstructuredContent(), annotations, "metadata", "annotations"); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| _, err = v.Dynamic.Resource(hyperConvergedGvr).Namespace("openshift-cnv").Update(context.Background(), hco, v1.UpdateOptions{}) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Creates target namespace if needed, and waits for it to exist | ||
| func (v *VirtOperator) ensureNamespace(timeout time.Duration) error { | ||
| if !v.checkNamespace() { | ||
|
|
@@ -513,6 +581,150 @@ func (v *VirtOperator) ensureHcoRemoved(timeout time.Duration) error { | |
| return err | ||
| } | ||
|
|
||
| func (v *VirtOperator) getVmStatus(namespace, name string) (string, error) { | ||
| vm, err := v.Dynamic.Resource(virtualMachineGvr).Namespace(namespace).Get(context.Background(), name, v1.GetOptions{}) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| status, ok, err := unstructured.NestedString(vm.UnstructuredContent(), "status", "printableStatus") | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| if !ok { | ||
| return "", fmt.Errorf("status field not populated yet on VM %s/%s", namespace, name) | ||
| } | ||
| log.Printf("VM %s/%s status is: %s", namespace, name, status) | ||
|
|
||
| return status, nil | ||
| } | ||
|
|
||
| func (v *VirtOperator) checkVmExists(namespace, name string) bool { | ||
| _, err := v.getVmStatus(namespace, name) | ||
| if err == nil { | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func (v *VirtOperator) checkVmStatus(namespace, name, expectedStatus string) bool { | ||
| status, _ := v.getVmStatus(namespace, name) | ||
| return status == expectedStatus | ||
| } | ||
|
|
||
| func (v *VirtOperator) createVm(namespace, name, source string) error { | ||
| unstructuredVm := unstructured.Unstructured{ | ||
| Object: map[string]interface{}{ | ||
mateusoliveira43 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "apiVersion": "kubevirt.io/v1", | ||
| "kind": "VirtualMachine", | ||
| "metadata": map[string]interface{}{ | ||
| "name": name, | ||
| "namespace": namespace, | ||
| }, | ||
| "spec": map[string]interface{}{ | ||
| "running": true, | ||
| "template": map[string]interface{}{ | ||
| "spec": map[string]interface{}{ | ||
| "domain": map[string]interface{}{ | ||
| "devices": map[string]interface{}{ | ||
| "disks": []map[string]interface{}{ | ||
| { | ||
| "disk": map[string]interface{}{ | ||
| "bus": "virtio", | ||
| }, | ||
| "name": "rootdisk", | ||
| }, | ||
| }, | ||
| }, | ||
| "resources": map[string]interface{}{ | ||
| "requests": map[string]interface{}{ | ||
| "cpu": "1", | ||
| "memory": "256Mi", | ||
| }, | ||
| }, | ||
| }, | ||
| "volumes": []map[string]interface{}{ | ||
| { | ||
| "dataVolume": map[string]interface{}{ | ||
| "name": name, | ||
| }, | ||
| "name": "rootdisk", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| if _, err := v.Dynamic.Resource(virtualMachineGvr).Namespace(namespace).Create(context.TODO(), &unstructuredVm, v1.CreateOptions{}); err != nil { | ||
| return fmt.Errorf("error creating VM %s/%s: %w", namespace, name, err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (v *VirtOperator) removeVm(namespace, name string) error { | ||
| if err := v.Dynamic.Resource(virtualMachineGvr).Namespace(namespace).Delete(context.TODO(), name, v1.DeleteOptions{}); err != nil { | ||
| if !apierrors.IsNotFound(err) { | ||
| return fmt.Errorf("error deleting VM %s/%s: %w", namespace, name, err) | ||
| } | ||
| log.Printf("VM %s/%s not found, delete not necessary.", namespace, name) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (v *VirtOperator) ensureVm(namespace, name, source string, timeout time.Duration) error { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here the logic is right? It will check if VM exists (without checking status), but if it fails, then it will check status
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe not, it doesn't look like I thought this through. The existence check is really there to speed up development of the CI test itself, since I don't have it tear down and recreate everything from scratch on every run. The status check polling is for the newly created VM. I can probably take out the existence check, since Kubernetes will complain if the VM is already there when the test tries to create it. |
||
| if err := v.createVm(namespace, name, source); err != nil { | ||
| return fmt.Errorf("failed to create VM %s/%s: %w", namespace, name, err) | ||
| } | ||
|
|
||
| err := wait.PollImmediate(5*time.Second, timeout, func() (bool, error) { | ||
| return v.checkVmStatus(namespace, name, "Running"), nil | ||
| }) | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| func (v *VirtOperator) ensureVmRemoval(namespace, name string, timeout time.Duration) error { | ||
| if !v.checkVmExists(namespace, name) { | ||
| log.Printf("VM %s/%s already removed, no action required", namespace, name) | ||
| return nil | ||
| } | ||
|
|
||
| if err := v.removeVm(namespace, name); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err := wait.PollImmediate(5*time.Second, timeout, func() (bool, error) { | ||
| return !v.checkVmExists(namespace, name), nil | ||
| }) | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| // Enable KVM emulation for use on cloud clusters that do not have direct | ||
| // access to the host server's virtualization capabilities. | ||
| func (v *VirtOperator) EnsureEmulation(timeout time.Duration) error { | ||
| if v.checkEmulation() { | ||
| log.Printf("KVM emulation already enabled, no work needed to turn it on.") | ||
| return nil | ||
| } | ||
|
|
||
| log.Printf("Enabling KVM emulation...") | ||
|
|
||
| if err := v.configureEmulation(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err := wait.PollImmediate(5*time.Second, timeout, func() (bool, error) { | ||
| return v.checkEmulation(), nil | ||
| }) | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| // IsVirtInstalled returns whether or not the OpenShift Virtualization operator | ||
| // is installed and ready, by checking for a HyperConverged operator resource. | ||
| func (v *VirtOperator) IsVirtInstalled() bool { | ||
|
|
@@ -525,7 +737,7 @@ func (v *VirtOperator) IsVirtInstalled() bool { | |
|
|
||
| // EnsureVirtInstallation makes sure the OpenShift Virtualization operator is | ||
| // installed. This will install the operator if it is not already present. | ||
| func (v *VirtOperator) EnsureVirtInstallation(timeout time.Duration) error { | ||
| func (v *VirtOperator) EnsureVirtInstallation() error { | ||
| if v.IsVirtInstalled() { | ||
| log.Printf("Virtualization operator already installed, no action needed") | ||
| return nil | ||
|
|
@@ -565,7 +777,7 @@ func (v *VirtOperator) EnsureVirtInstallation(timeout time.Duration) error { | |
| } | ||
|
|
||
| // EnsureVirtRemoval makes sure the virtualization operator is removed. | ||
| func (v *VirtOperator) EnsureVirtRemoval(timeout time.Duration) error { | ||
| func (v *VirtOperator) EnsureVirtRemoval() error { | ||
| log.Printf("Removing hyperconverged operator") | ||
| if err := v.ensureHcoRemoved(3 * time.Minute); err != nil { | ||
| return err | ||
|
|
@@ -598,3 +810,15 @@ func (v *VirtOperator) EnsureVirtRemoval(timeout time.Duration) error { | |
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Create a virtual machine from an existing PVC. | ||
| func (v *VirtOperator) CreateVm(namespace, name, source string, timeout time.Duration) error { | ||
| log.Printf("Creating virtual machine %s/%s", namespace, name) | ||
| return v.ensureVm(namespace, name, source, timeout) | ||
| } | ||
|
|
||
| // Remove a virtual machine, but leave its data volume. | ||
| func (v *VirtOperator) RemoveVm(namespace, name string, timeout time.Duration) error { | ||
| log.Printf("Removing virtual machine %s/%s", namespace, name) | ||
| return v.ensureVmRemoval(namespace, name, timeout) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is KVM?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's referring to this qemu KVM . Read more at wiki
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
KVM here means kernel virtual machine, which is a kernel feature that accelerates virtual machines using the virtualization extensions provided by the CPU. Most cloud clusters do not have direct access to the CPU, and can't use KVM. So this pull request turns on an emulation mode that is much slower, but will work on our CI instances.