Skip to content

Commit

Permalink
add specchanges for vmclass
Browse files Browse the repository at this point in the history
Signed-off-by: yaroslavborbat <[email protected]>
  • Loading branch information
yaroslavborbat committed Dec 5, 2024
1 parent 64effd2 commit ebd0bb8
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 10 deletions.
3 changes: 3 additions & 0 deletions api/core/v1alpha2/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const (
// ReasonVMLastAppliedSpecInvalid is event reason that JSON in last-applied-spec annotation is invalid.
ReasonVMLastAppliedSpecInvalid = "VMLastAppliedSpecInvalid"

// ReasonVMClassLastAppliedSpecInvalid is event reason that JSON in last-applied-spec annotation is invalid.
ReasonVMClassLastAppliedSpecInvalid = "VMClassLastAppliedSpecInvalid"

// ReasonErrVmNotSynced is event reason that vm is not synced.
ReasonErrVmNotSynced = "VirtualMachineNotSynced"

Expand Down
7 changes: 6 additions & 1 deletion api/core/v1alpha2/virtual_machine_class.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ type VirtualMachineClassList struct {
Items []VirtualMachineClass `json:"items"`
}

type VirtualMachineClassNodePlacement struct {
NodeSelector NodeSelector `json:"nodeSelector,omitempty"`
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
}

type VirtualMachineClassSpec struct {
NodeSelector NodeSelector `json:"nodeSelector,omitempty"`
// Tolerations are the same as `spec.tolerations` in the [Pod](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/).
Expand Down Expand Up @@ -208,7 +213,7 @@ type VirtualMachineClassStatus struct {
// +kubebuilder:example={"maxAllocatableResources: {\"cpu\": 1, \"memory\": \"10Gi\"}"}
MaxAllocatableResources corev1.ResourceList `json:"maxAllocatableResources,omitempty"`
// The latest detailed observations of the VirtualMachineClass resource.
Conditions []metav1.Condition `json:"conditions,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
// The generation last processed by the controller.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
Expand Down
3 changes: 3 additions & 0 deletions images/virtualization-artifact/pkg/controller/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ const (
// AnnVMLastAppliedSpec is an annotation on KVVM. It contains a JSON with VM spec.
AnnVMLastAppliedSpec = AnnAPIGroup + "/vm.last-applied-spec"

// AnnVMClassLastAppliedSpec is an annotation on KVVM. It contains a JSON with VM spec.
AnnVMClassLastAppliedSpec = AnnAPIGroup + "/vmclass.last-applied-spec"

// LastPropagatedVMAnnotationsAnnotation is a marshalled map of previously applied virtual machine annotations.
LastPropagatedVMAnnotationsAnnotation = AnnAPIGroup + "/last-propagated-vm-annotations"
// LastPropagatedVMLabelsAnnotation is a marshalled map of previously applied virtual machine labels.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,29 @@ func SetLastAppliedSpec(kvvm *virtv1.VirtualMachine, vm *v1alpha2.VirtualMachine
common.AddAnnotation(kvvm, common.AnnVMLastAppliedSpec, string(lastApplied))
return nil
}

// LoadLastAppliedClassSpec loads VMClass spec from JSON in the last-applied-spec annotation.
func LoadLastAppliedClassSpec(kvvm *virtv1.VirtualMachine) (*v1alpha2.VirtualMachineClassSpec, error) {
lastSpecJSON := kvvm.GetAnnotations()[common.AnnVMClassLastAppliedSpec]
if strings.TrimSpace(lastSpecJSON) == "" {
return nil, nil
}

var spec v1alpha2.VirtualMachineClassSpec
err := json.Unmarshal([]byte(lastSpecJSON), &spec)
if err != nil {
return nil, fmt.Errorf("load spec from JSON: %w", err)
}
return &spec, nil
}

// SetLastAppliedClassSpec updates the last-applied-spec annotation with VMClass spec JSON.
func SetLastAppliedClassSpec(kvvm *virtv1.VirtualMachine, class *v1alpha2.VirtualMachineClass) error {
lastApplied, err := json.Marshal(class.Spec)
if err != nil {
return fmt.Errorf("convert spec to JSON: %w", err)
}

common.AddAnnotation(kvvm, common.AnnVMClassLastAppliedSpec, string(lastApplied))
return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,18 @@ func (h *SyncKvvmHandler) Handle(ctx context.Context, s state.VirtualMachineStat
Message(service.CapitalizeFirstLetter(err.Error()) + ".")
return reconcile.Result{}, err
}
class, err := s.Class(ctx)
if err != nil {
return reconcile.Result{}, err
}

// 1. Set RestartAwaitingChanges.
var lastAppliedSpec *virtv2.VirtualMachineSpec
var changes vmchange.SpecChanges
if kvvm != nil {
lastAppliedSpec = h.loadLastAppliedSpec(current, kvvm)
changes = h.detectSpecChanges(ctx, kvvm, &current.Spec, lastAppliedSpec)
lastClassAppliedSpec := h.loadClassLastAppliedSpec(class, kvvm)
changes = h.detectSpecChanges(ctx, kvvm, &current.Spec, lastAppliedSpec, &class.Spec, lastClassAppliedSpec)
}

if kvvm == nil || changes.IsEmpty() {
Expand Down Expand Up @@ -375,7 +380,12 @@ func (h *SyncKvvmHandler) makeKVVMFromVMSpec(ctx context.Context, s state.Virtua

err = kvbuilder.SetLastAppliedSpec(newKVVM, current)
if err != nil {
return nil, fmt.Errorf("set last applied spec on the internal virtual machine: %w", err)
return nil, fmt.Errorf("set vm last applied spec on the internal virtual machine: %w", err)
}

err = kvbuilder.SetLastAppliedClassSpec(newKVVM, class)
if err != nil {
return nil, fmt.Errorf("set vmclass last applied spec on the internal virtual machine: %w", err)
}

return newKVVM, nil
Expand Down Expand Up @@ -417,9 +427,34 @@ func (h *SyncKvvmHandler) loadLastAppliedSpec(vm *virtv2.VirtualMachine, kvvm *v
return lastSpec
}

func (h *SyncKvvmHandler) loadClassLastAppliedSpec(class *virtv2.VirtualMachineClass, kvvm *virtv1.VirtualMachine) *virtv2.VirtualMachineClassSpec {
if kvvm == nil || class == nil {
return nil
}

lastSpec, err := kvbuilder.LoadLastAppliedClassSpec(kvvm)
// TODO Add smarter handler for empty/invalid annotation.
if lastSpec == nil && err == nil {
h.recorder.Event(class, corev1.EventTypeWarning, virtv2.ReasonVMClassLastAppliedSpecInvalid, "Could not find last applied spec. Possible old VMClass or partial backup restore. Restart or recreate VM to adopt it.")
lastSpec = &virtv2.VirtualMachineClassSpec{}
}
if err != nil {
msg := fmt.Sprintf("Could not restore last applied spec: %v. Possible old VMClass or partial backup restore. Restart or recreate VM to adopt it.", err)
h.recorder.Event(class, corev1.EventTypeWarning, virtv2.ReasonVMClassLastAppliedSpecInvalid, msg)
lastSpec = &virtv2.VirtualMachineClassSpec{}
}

return lastSpec
}

// detectSpecChanges compares KVVM generated from current VM spec with in cluster KVVM
// to calculate changes and action needed to apply these changes.
func (h *SyncKvvmHandler) detectSpecChanges(ctx context.Context, kvvm *virtv1.VirtualMachine, currentSpec, lastSpec *virtv2.VirtualMachineSpec) vmchange.SpecChanges {
func (h *SyncKvvmHandler) detectSpecChanges(
ctx context.Context,
kvvm *virtv1.VirtualMachine,
currentSpec, lastSpec *virtv2.VirtualMachineSpec,
currentClassSpec, lastClassSpec *virtv2.VirtualMachineClassSpec,
) vmchange.SpecChanges {
log := logger.FromContext(ctx)

// Not applicable if KVVM is absent.
Expand All @@ -429,7 +464,7 @@ func (h *SyncKvvmHandler) detectSpecChanges(ctx context.Context, kvvm *virtv1.Vi

// Compare VM spec applied to the underlying KVVM
// with the current VM spec (maybe edited by the user).
specChanges := vmchange.CompareSpecs(lastSpec, currentSpec)
specChanges := vmchange.CompareSpecs(lastSpec, currentSpec, currentClassSpec, lastClassSpec)

log.Info(fmt.Sprintf("detected changes: empty %v, disruptive %v, actionType %v", specChanges.IsEmpty(), specChanges.IsDisruptive(), specChanges.ActionType()))
log.Info(fmt.Sprintf("detected changes JSON: %s", specChanges.ToJSON()))
Expand Down Expand Up @@ -515,22 +550,35 @@ func (h *SyncKvvmHandler) applyVMChangesToKVVM(ctx context.Context, s state.Virt
case vmchange.ActionNone:
log.Info("No changes to underlying KVVM, update last-applied-spec annotation", "vm.name", current.GetName())

if err := h.updateKVVMLastAppliedSpec(ctx, current, kvvm); err != nil {
class, err := s.Class(ctx)
if err != nil {
return fmt.Errorf("failed to get vmclass: %w", err)
}
if err = h.updateKVVMLastAppliedSpec(ctx, current, kvvm, class); err != nil {
return fmt.Errorf("unable to update last-applied-spec on KVVM: %w", err)
}
}
return nil
}

// updateKVVMLastAppliedSpec updates last-applied-spec annotation on KubeVirt VirtualMachine.
func (h *SyncKvvmHandler) updateKVVMLastAppliedSpec(ctx context.Context, vm *virtv2.VirtualMachine, kvvm *virtv1.VirtualMachine) error {
func (h *SyncKvvmHandler) updateKVVMLastAppliedSpec(
ctx context.Context,
vm *virtv2.VirtualMachine,
kvvm *virtv1.VirtualMachine,
class *virtv2.VirtualMachineClass,
) error {
if vm == nil || kvvm == nil {
return nil
}

err := kvbuilder.SetLastAppliedSpec(kvvm, vm)
if err != nil {
return fmt.Errorf("set last applied spec on KubeVirt VM '%s': %w", kvvm.GetName(), err)
return fmt.Errorf("set vm last applied spec on KubeVirt VM '%s': %w", kvvm.GetName(), err)
}
err = kvbuilder.SetLastAppliedClassSpec(kvvm, class)
if err != nil {
return fmt.Errorf("set vmclass last applied spec on KubeVirt VM '%s': %w", kvvm.GetName(), err)
}

if err := h.client.Update(ctx, kvvm); err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,21 @@ var specComparators = []SpecFieldsComparator{
compareProvisioning,
}

func CompareSpecs(prev, next *v1alpha2.VirtualMachineSpec) SpecChanges {
type VmClassSpecFieldsComparator func(prev, next *v1alpha2.VirtualMachineClassSpec) []FieldChange

var vmclassSpecComparators = []VmClassSpecFieldsComparator{
compareVmClassNodeSelector,
compareVmClassTolerations,
}

func CompareSpecs(prev, next *v1alpha2.VirtualMachineSpec, prevClass, nextClass *v1alpha2.VirtualMachineClassSpec) SpecChanges {
specChanges := CompareVMSpecs(prev, next)
specClassChanges := CompareClassSpecs(prevClass, nextClass)
specChanges.Add(specClassChanges.GetAll()...)
return specChanges
}

func CompareVMSpecs(prev, next *v1alpha2.VirtualMachineSpec) SpecChanges {
specChanges := SpecChanges{}

for _, comparator := range specComparators {
Expand All @@ -54,3 +68,16 @@ func CompareSpecs(prev, next *v1alpha2.VirtualMachineSpec) SpecChanges {

return specChanges
}

func CompareClassSpecs(prevClass, nextClass *v1alpha2.VirtualMachineClassSpec) SpecChanges {
var specChanges SpecChanges

for _, comparator := range vmclassSpecComparators {
changes := comparator(prevClass, nextClass)
if HasChanges(changes) {
specChanges.Add(changes...)
}
}

return specChanges
}
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ enableParavirtualization: true
currentSpec := loadVMSpec(t, tt.currentSpec)
desiredSpec := loadVMSpec(t, tt.desiredSpec)

changes = CompareSpecs(currentSpec, desiredSpec)
changes = CompareVMSpecs(currentSpec, desiredSpec)

defer func() {
if t.Failed() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package vmchange

import (
"fmt"
"reflect"

"github.com/deckhouse/virtualization/api/core/v1alpha2"
)

func makePathWithClass(path string) string {
return fmt.Sprintf("VirtualMachineClass:%s", path)
}

func compareVmClassNodeSelector(current, desired *v1alpha2.VirtualMachineClassSpec) []FieldChange {
isEmpty := func(nodeSelector v1alpha2.NodeSelector) bool {
return len(nodeSelector.MatchExpressions) == 0 && len(nodeSelector.MatchLabels) == 0
}

currentValue := NewValue(current.NodeSelector, isEmpty(current.NodeSelector), false)
desiredValue := NewValue(desired.NodeSelector, isEmpty(desired.NodeSelector), false)

return compareValues(
makePathWithClass("spec.nodeSelector"),
currentValue,
desiredValue,
reflect.DeepEqual(current.NodeSelector, desired.NodeSelector),
ActionRestart,
)
}

func compareVmClassTolerations(current, desired *v1alpha2.VirtualMachineClassSpec) []FieldChange {
currentValue := NewValue(current.Tolerations, len(current.Tolerations) == 0, false)
desiredValue := NewValue(desired.Tolerations, len(desired.Tolerations) == 0, false)

return compareValues(
makePathWithClass("spec.tolerations"),
currentValue,
desiredValue,
reflect.DeepEqual(current.Tolerations, desired.Tolerations),
ActionRestart,
)
}

0 comments on commit ebd0bb8

Please sign in to comment.