Skip to content

Commit

Permalink
instancetype: Support default instance type PVC annotations
Browse files Browse the repository at this point in the history
This change introduces support for inferring the default instance type
and instance type kind from a specific Volume using the recently
introduced `inferFromVolume` InstancetypeMatcher attribute.

The following combinations are supported by this change:

Volume -> PersistentVolumeClaimVolumeSource -> PersistentVolumeClaim
Volume -> DataVolumeSource -> DataVolumeSourcePVC -> PersistentVolumeClaim
Volume -> DataVolumeSource -> DataVolumeSourceRef -> DataSource -> PersistentVolumeClaim
Volume -> DataVolumeSource -> DataVolumeTemplates -> DataVolumeSourcePVC -> PersistentVolumeClaim
Volume -> DataVolumeSource -> DataVolumeTemplates -> DataVolumeSourceRef -> DataSource -> PersistentVolumeClaim

When a valid Volume name is provided the VirtualMachine mutation webhook
will now attempt to find an underlying PersistentVolumeClaim for the
Volume, checking for the following annotations:

* instancetype.kubevirt.io/default-instancetype

This is required when a Volume name has been provided by
inferFromVolume, failure to find this on the underlying
PersistentVolumeClaim will cause the request to be rejected.

This simply provides the name of the default instance type.

* instancetype.kubevirt.io/default-instancetype-kind

This is optional, when not provided the existing default of
VirtualMachineClusterInstancetype will be applied.

The InstancetypeMatcher of the VirtualMachine is recreated using these
values before the request is passed to the validation webhook where the
existing InstancetypeMatcher checks for conflicts etc are processed.

Signed-off-by: Lee Yarwood <[email protected]>
  • Loading branch information
lyarwood committed Dec 8, 2022
1 parent 0ceefe2 commit 3bd997b
Show file tree
Hide file tree
Showing 7 changed files with 599 additions and 4 deletions.
1 change: 1 addition & 0 deletions pkg/instancetype/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/k8s.io/utils/pointer:go_default_library",
"//vendor/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1:go_default_library",
],
)

Expand Down
102 changes: 102 additions & 0 deletions pkg/instancetype/instancetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
generatedscheme "kubevirt.io/client-go/generated/kubevirt/clientset/versioned/scheme"
"kubevirt.io/client-go/kubecli"
"kubevirt.io/client-go/log"
"kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"

utils "kubevirt.io/kubevirt/pkg/util"
utiltypes "kubevirt.io/kubevirt/pkg/util/types"
Expand All @@ -32,6 +33,7 @@ type Methods interface {
ApplyToVmi(field *k8sfield.Path, instancetypespec *instancetypev1alpha2.VirtualMachineInstancetypeSpec, prefernceSpec *instancetypev1alpha2.VirtualMachinePreferenceSpec, vmiSpec *virtv1.VirtualMachineInstanceSpec) Conflicts
FindPreferenceSpec(vm *virtv1.VirtualMachine) (*instancetypev1alpha2.VirtualMachinePreferenceSpec, error)
StoreControllerRevisions(vm *virtv1.VirtualMachine) error
InferDefaultInstancetype(vm *virtv1.VirtualMachine) (*virtv1.InstancetypeMatcher, error)
}

type Conflicts []*k8sfield.Path
Expand Down Expand Up @@ -521,6 +523,106 @@ func (m *methods) findClusterInstancetype(vm *virtv1.VirtualMachine) (*instancet
return instancetype, nil
}

func (m *methods) InferDefaultInstancetype(vm *virtv1.VirtualMachine) (*virtv1.InstancetypeMatcher, error) {
if vm.Spec.Instancetype == nil || vm.Spec.Instancetype.InferFromVolume == "" {
return nil, nil
}
defaultName, defaultKind, err := m.inferDefaultsFromVolumes(vm, vm.Spec.Instancetype.InferFromVolume, apiinstancetype.DefaultInstancetypeAnnotation, apiinstancetype.DefaultInstancetypeKindAnnotation)
if err != nil {
return nil, err
}
return &v1.InstancetypeMatcher{
Name: defaultName,
Kind: defaultKind,
}, nil
}

/*
Defaults will be inferred from the following combinations of DataVolumeSources, DataVolumeTemplates, DataSources and PVCs:
Volume -> PersistentVolumeClaimVolumeSource -> PersistentVolumeClaim
Volume -> DataVolumeSource -> DataVolumeSourcePVC -> PersistentVolumeClaim
Volume -> DataVolumeSource -> DataVolumeSourceRef -> DataSource -> PersistentVolumeClaim
Volume -> DataVolumeSource -> DataVolumeTemplate -> DataVolumeSourcePVC -> PersistentVolumeClaim
Volume -> DataVolumeSource -> DataVolumeTemplate -> DataVolumeSourceRef -> DataSource -> PersistentVolumeClaim
*/
func (m *methods) inferDefaultsFromVolumes(vm *virtv1.VirtualMachine, inferFromVolumeName, defaultNameAnnotation, defaultKindAnnotation string) (string, string, error) {
for _, volume := range vm.Spec.Template.Spec.Volumes {
if volume.Name != inferFromVolumeName {
continue
}
if volume.PersistentVolumeClaim != nil {
return m.inferDefaultsFromPVC(volume.PersistentVolumeClaim.ClaimName, vm.Namespace, defaultNameAnnotation, defaultKindAnnotation)
}
if volume.DataVolume != nil {
return m.inferDefaultsFromDataVolume(vm, volume.DataVolume.Name, defaultNameAnnotation, defaultKindAnnotation)
}
return "", "", fmt.Errorf("unable to infer defaults from volume %s as type is not supported", inferFromVolumeName)
}
return "", "", fmt.Errorf("unable to find volume %s to infer defaults", inferFromVolumeName)
}

func inferDefaultsFromAnnotations(annotations map[string]string, defaultNameAnnotation, defaultKindAnnotation string) (string, string, error) {
defaultName, hasAnnotation := annotations[defaultNameAnnotation]
if !hasAnnotation {
return "", "", fmt.Errorf("unable to find required %s annotation on the volume", defaultNameAnnotation)
}
return defaultName, annotations[defaultKindAnnotation], nil
}

func (m *methods) inferDefaultsFromPVC(pvcName, pvcNamespace, defaultNameAnnotation, defaultKindAnnotation string) (string, string, error) {
pvc, err := m.clientset.CoreV1().PersistentVolumeClaims(pvcNamespace).Get(context.Background(), pvcName, metav1.GetOptions{})
if err != nil {
return "", "", err
}
return inferDefaultsFromAnnotations(pvc.Annotations, defaultNameAnnotation, defaultKindAnnotation)
}

func (m *methods) inferDefaultsFromDataVolume(vm *virtv1.VirtualMachine, dvName, defaultNameAnnotation, defaultKindAnnotation string) (string, string, error) {
if len(vm.Spec.DataVolumeTemplates) > 0 {
for _, dvt := range vm.Spec.DataVolumeTemplates {
if dvt.Name != dvName {
continue
}
return m.inferDefaultsFromDataVolumeSpec(&dvt.Spec, defaultNameAnnotation, defaultKindAnnotation)
}
}
dv, err := m.clientset.CdiClient().CdiV1beta1().DataVolumes(vm.Namespace).Get(context.Background(), dvName, metav1.GetOptions{})
if err != nil {
// Handle garbage collected DataVolumes by attempting to lookup the PVC using the name of the DataVolume in the VM namespace
if errors.IsNotFound(err) {
return m.inferDefaultsFromPVC(dvName, vm.Namespace, defaultNameAnnotation, defaultKindAnnotation)
}
return "", "", err
}
return m.inferDefaultsFromDataVolumeSpec(&dv.Spec, defaultNameAnnotation, defaultKindAnnotation)
}

func (m *methods) inferDefaultsFromDataVolumeSpec(dataVolumeSpec *v1beta1.DataVolumeSpec, defaultNameAnnotation, defaultKindAnnotation string) (string, string, error) {
if dataVolumeSpec != nil && dataVolumeSpec.Source != nil && dataVolumeSpec.Source.PVC != nil {
return m.inferDefaultsFromPVC(dataVolumeSpec.Source.PVC.Name, dataVolumeSpec.Source.PVC.Namespace, defaultNameAnnotation, defaultKindAnnotation)
}
if dataVolumeSpec != nil && dataVolumeSpec.SourceRef != nil {
return m.inferDefaultsFromDataVolumeSourceRef(dataVolumeSpec.SourceRef, defaultNameAnnotation, defaultKindAnnotation)
}
return "", "", fmt.Errorf("unable to infer defaults from DataVolumeSpec as DataVolumeSource is not supported")
}

func (m *methods) inferDefaultsFromDataVolumeSourceRef(sourceRef *v1beta1.DataVolumeSourceRef, defaultNameAnnotation, defaultKindAnnotation string) (string, string, error) {
switch sourceRef.Kind {
case "DataSource":
dataSource, err := m.clientset.CdiClient().CdiV1beta1().DataSources(*sourceRef.Namespace).Get(context.Background(), sourceRef.Name, metav1.GetOptions{})
if err != nil {
return "", "", err
}
if dataSource.Spec.Source.PVC != nil {
return m.inferDefaultsFromPVC(dataSource.Spec.Source.PVC.Name, dataSource.Spec.Source.PVC.Namespace, defaultNameAnnotation, defaultKindAnnotation)
}
return "", "", fmt.Errorf("unable to infer defaults from DataSource that doesn't provide DataVolumeSourcePVC")
}
return "", "", fmt.Errorf("unable to infer defaults from DataVolumeSourceRef as Kind %s is not supported", sourceRef.Kind)
}

func applyCpu(field *k8sfield.Path, instancetypeSpec *instancetypev1alpha2.VirtualMachineInstancetypeSpec, preferenceSpec *instancetypev1alpha2.VirtualMachinePreferenceSpec, vmiSpec *virtv1.VirtualMachineInstanceSpec) Conflicts {
if vmiSpec.Domain.CPU != nil {
return Conflicts{field.Child("domain", "cpu")}
Expand Down
8 changes: 8 additions & 0 deletions pkg/testutils/mock_flavor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type MockInstancetypeMethods struct {
ApplyToVmiFunc func(field *k8sfield.Path, instancetypespec *instancetypev1alpha2.VirtualMachineInstancetypeSpec, preferenceSpec *instancetypev1alpha2.VirtualMachinePreferenceSpec, vmiSpec *v1.VirtualMachineInstanceSpec) instancetype.Conflicts
FindPreferenceSpecFunc func(vm *v1.VirtualMachine) (*instancetypev1alpha2.VirtualMachinePreferenceSpec, error)
StoreControllerRevisionsFunc func(vm *v1.VirtualMachine) error
InferDefaultInstancetypeFunc func(vm *v1.VirtualMachine) (*v1.InstancetypeMatcher, error)
}

var _ instancetype.Methods = &MockInstancetypeMethods{}
Expand All @@ -34,6 +35,10 @@ func (m *MockInstancetypeMethods) StoreControllerRevisions(vm *v1.VirtualMachine
return m.StoreControllerRevisionsFunc(vm)
}

func (m *MockInstancetypeMethods) InferDefaultInstancetype(vm *v1.VirtualMachine) (*v1.InstancetypeMatcher, error) {
return m.InferDefaultInstancetypeFunc(vm)
}

func NewMockInstancetypeMethods() *MockInstancetypeMethods {
return &MockInstancetypeMethods{
FindInstancetypeSpecFunc: func(_ *v1.VirtualMachine) (*instancetypev1alpha2.VirtualMachineInstancetypeSpec, error) {
Expand All @@ -48,5 +53,8 @@ func NewMockInstancetypeMethods() *MockInstancetypeMethods {
StoreControllerRevisionsFunc: func(_ *v1.VirtualMachine) error {
return nil
},
InferDefaultInstancetypeFunc: func(_ *v1.VirtualMachine) (*v1.InstancetypeMatcher, error) {
return nil, nil
},
}
}
2 changes: 2 additions & 0 deletions pkg/virt-api/webhooks/mutating-webhook/mutators/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ go_test(
"//staging/src/kubevirt.io/api/instancetype:go_default_library",
"//staging/src/kubevirt.io/api/instancetype/v1alpha2:go_default_library",
"//staging/src/kubevirt.io/client-go/api:go_default_library",
"//staging/src/kubevirt.io/client-go/generated/containerized-data-importer/clientset/versioned/fake:go_default_library",
"//staging/src/kubevirt.io/client-go/generated/kubevirt/clientset/versioned/fake:go_default_library",
"//staging/src/kubevirt.io/client-go/generated/kubevirt/clientset/versioned/typed/instancetype/v1alpha2:go_default_library",
"//staging/src/kubevirt.io/client-go/kubecli:go_default_library",
Expand All @@ -81,6 +82,7 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/utils/pointer:go_default_library",
"//vendor/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1:go_default_library",
Expand Down
20 changes: 20 additions & 0 deletions pkg/virt-api/webhooks/mutating-webhook/mutators/vm-mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ func (mutator *VMsMutator) Mutate(ar *admissionv1.AdmissionReview) *admissionv1.

// Set VM defaults
log.Log.Object(&vm).V(4).Info("Apply defaults")

if err = mutator.inferDefaultInstancetype(&vm); err != nil {
log.Log.Reason(err).Error("admission failed, unable to set default instancetype")
return &admissionv1.AdmissionResponse{
Result: &metav1.Status{
Message: err.Error(),
Code: http.StatusBadRequest,
},
}
}

mutator.setDefaultInstancetypeKind(&vm)
mutator.setDefaultPreferenceKind(&vm)
preferenceSpec := mutator.getPreferenceSpec(&vm)
Expand Down Expand Up @@ -151,6 +162,15 @@ func (mutator *VMsMutator) setPreferenceStorageClassName(vm *v1.VirtualMachine,
}
}

func (mutator *VMsMutator) inferDefaultInstancetype(vm *v1.VirtualMachine) error {
instancetypeMatcher, err := mutator.InstancetypeMethods.InferDefaultInstancetype(vm)
if err != nil {
return err
}
vm.Spec.Instancetype = instancetypeMatcher
return nil
}

func (mutator *VMsMutator) setDefaultInstancetypeKind(vm *v1.VirtualMachine) {
if vm.Spec.Instancetype == nil {
return
Expand Down
Loading

0 comments on commit 3bd997b

Please sign in to comment.