View
@@ -0,0 +1,57 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package config_disk
import (
"path/filepath"
"kubevirt.io/kubevirt/pkg/api/v1"
)
// GetSecretSourcePath returns a path to Secret mounted on a pod
func GetSecretSourcePath(volumeName string) string {
return filepath.Join(SecretSourceDir, volumeName)
}
// GetSecretDiskPath returns a path to Secret iso image created based on volume name
func GetSecretDiskPath(volumeName string) string {
return filepath.Join(SecretDisksDir, volumeName+".iso")
}
// CreateSecretDisks creates Secret iso disks which are attached to vmis
func CreateSecretDisks(vmi *v1.VirtualMachineInstance) error {
for _, volume := range vmi.Spec.Volumes {
if volume.Secret != nil {
var filesPath []string
filesPath, err := getFilesLayout(GetSecretSourcePath(volume.Name))
if err != nil {
return err
}
err = createIsoConfigImage(GetSecretDiskPath(volume.Name), filesPath)
if err != nil {
return err
}
}
}
return nil
}
View
@@ -0,0 +1,70 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package config_disk
import (
"io/ioutil"
"os"
"path/filepath"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"kubevirt.io/kubevirt/pkg/api/v1"
)
var _ = Describe("Secret", func() {
BeforeEach(func() {
var err error
SecretSourceDir, err = ioutil.TempDir("", "secret")
Expect(err).NotTo(HaveOccurred())
os.MkdirAll(filepath.Join(SecretSourceDir, "secret-volume", "test-dir"), 0755)
os.OpenFile(filepath.Join(SecretSourceDir, "secret-volume", "test-dir", "test-file1"), os.O_RDONLY|os.O_CREATE, 0666)
os.OpenFile(filepath.Join(SecretSourceDir, "secret-volume", "test-file2"), os.O_RDONLY|os.O_CREATE, 0666)
SecretDisksDir, err = ioutil.TempDir("", "secret-disks")
Expect(err).NotTo(HaveOccurred())
})
AfterEach(func() {
os.RemoveAll(SecretSourceDir)
os.RemoveAll(SecretDisksDir)
})
It("Should create a new secret iso disk", func() {
vmi := v1.NewMinimalVMI("fake-vmi")
vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{
Name: "secret-volume",
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{
SecretName: "test-secret",
},
},
})
err := CreateSecretDisks(vmi)
Expect(err).NotTo(HaveOccurred())
_, err = os.Stat(filepath.Join(SecretDisksDir, "secret-volume.iso"))
Expect(err).NotTo(HaveOccurred())
})
})
View
@@ -0,0 +1,65 @@
package config
import (
"strings"
"fmt"
"k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
)
const configMapName = "kube-system/kubevirt-config"
const useEmulationKey = "debug.useEmulation"
const imagePullPolicyKey = "dev.imagePullPolicy"
func NewClusterConfig(configMapInformer cache.Store) *ClusterConfig {
c := &ClusterConfig{
store: configMapInformer,
}
return c
}
type ClusterConfig struct {
store cache.Store
}
func (c *ClusterConfig) IsUseEmulation() (bool, error) {
useEmulationValue, err := getConfigMapEntry(c.store, useEmulationKey)
if err != nil || useEmulationValue == "" {
return false, err
}
if useEmulationValue == "" {
}
return (strings.ToLower(useEmulationValue) == "true"), nil
}
func (c *ClusterConfig) GetImagePullPolicy() (policy v1.PullPolicy, err error) {
var value string
if value, err = getConfigMapEntry(c.store, imagePullPolicyKey); err != nil || value == "" {
policy = v1.PullIfNotPresent // Default if not specified
} else {
switch value {
case "Always":
policy = v1.PullAlways
case "Never":
policy = v1.PullNever
case "IfNotPresent":
policy = v1.PullIfNotPresent
default:
err = fmt.Errorf("Invalid ImagePullPolicy in ConfigMap: %s", value)
}
}
return
}
func getConfigMapEntry(store cache.Store, key string) (string, error) {
if obj, exists, err := store.GetByKey(configMapName); err != nil {
return "", err
} else if !exists {
return "", nil
} else {
return obj.(*v1.ConfigMap).Data[key], nil
}
}
View
@@ -0,0 +1,13 @@
package config_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestConfig(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Config Suite")
}
View
@@ -0,0 +1,170 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2017 Red Hat, Inc.
*
*/
package config
import (
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
kubev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
"kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/log"
)
var _ = Describe("ConfigMap", func() {
log.Log.SetIOWriter(GinkgoWriter)
var cmListWatch *cache.ListWatch
var cmInformer cache.SharedIndexInformer
var stopChan chan struct{}
BeforeEach(func() {
stopChan = make(chan struct{})
})
AfterEach(func() {
close(stopChan)
})
It("Should return false if configmap is not present", func() {
cmListWatch = MakeFakeConfigMapWatcher([]kubev1.ConfigMap{})
cmInformer = cache.NewSharedIndexInformer(cmListWatch, &v1.VirtualMachineInstance{}, time.Second, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
go cmInformer.Run(stopChan)
cache.WaitForCacheSync(stopChan, cmInformer.HasSynced)
clusterConfig := NewClusterConfig(cmInformer.GetStore())
result, err := clusterConfig.IsUseEmulation()
Expect(err).ToNot(HaveOccurred())
Expect(result).To(BeFalse())
})
It("Should return false if configmap doesn't have useEmulation set", func() {
cfgMap := kubev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "kube-system",
Name: "kubevirt-config",
},
Data: map[string]string{},
}
cmListWatch = MakeFakeConfigMapWatcher([]kubev1.ConfigMap{cfgMap})
cmInformer = cache.NewSharedIndexInformer(cmListWatch, &v1.VirtualMachineInstance{}, time.Second, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
go cmInformer.Run(stopChan)
cache.WaitForCacheSync(stopChan, cmInformer.HasSynced)
cache.WaitForCacheSync(stopChan, cmInformer.HasSynced)
clusterConfig := NewClusterConfig(cmInformer.GetStore())
result, err := clusterConfig.IsUseEmulation()
Expect(err).ToNot(HaveOccurred())
Expect(result).To(BeFalse())
})
It("Should return true if useEmulation = true", func() {
cfgMap := kubev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "kube-system",
Name: "kubevirt-config",
},
Data: map[string]string{"debug.useEmulation": "true"},
}
cmListWatch = MakeFakeConfigMapWatcher([]kubev1.ConfigMap{cfgMap})
cmInformer = cache.NewSharedIndexInformer(cmListWatch, &v1.VirtualMachineInstance{}, time.Second, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
go cmInformer.Run(stopChan)
cache.WaitForCacheSync(stopChan, cmInformer.HasSynced)
cache.WaitForCacheSync(stopChan, cmInformer.HasSynced)
clusterConfig := NewClusterConfig(cmInformer.GetStore())
result, err := clusterConfig.IsUseEmulation()
Expect(err).ToNot(HaveOccurred())
Expect(result).To(BeTrue())
})
It("Should return IfNotPresent if configmap doesn't have imagePullPolicy set", func() {
cfgMap := kubev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "kube-system",
Name: "kubevirt-config",
},
Data: map[string]string{},
}
cmListWatch = MakeFakeConfigMapWatcher([]kubev1.ConfigMap{cfgMap})
cmInformer = cache.NewSharedIndexInformer(cmListWatch, &v1.VirtualMachineInstance{}, time.Second, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
go cmInformer.Run(stopChan)
cache.WaitForCacheSync(stopChan, cmInformer.HasSynced)
result, err := NewClusterConfig(cmInformer.GetStore()).GetImagePullPolicy()
Expect(err).ToNot(HaveOccurred())
Expect(result).To(Equal(kubev1.PullIfNotPresent))
})
It("Should return Always if imagePullPolicy = Always", func() {
cfgMap := kubev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "kube-system",
Name: "kubevirt-config",
},
Data: map[string]string{imagePullPolicyKey: "Always"},
}
cmListWatch = MakeFakeConfigMapWatcher([]kubev1.ConfigMap{cfgMap})
cmInformer = cache.NewSharedIndexInformer(cmListWatch, &v1.VirtualMachineInstance{}, time.Second, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
go cmInformer.Run(stopChan)
cache.WaitForCacheSync(stopChan, cmInformer.HasSynced)
result, err := NewClusterConfig(cmInformer.GetStore()).GetImagePullPolicy()
Expect(err).ToNot(HaveOccurred())
Expect(result).To(Equal(kubev1.PullAlways))
})
It("Should return an error if imagePullPolicy is not valid", func() {
cfgMap := kubev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "kube-system",
Name: "kubevirt-config",
},
Data: map[string]string{imagePullPolicyKey: "IHaveNoStrongFeelingsOneWayOrTheOther"},
}
cmListWatch = MakeFakeConfigMapWatcher([]kubev1.ConfigMap{cfgMap})
cmInformer = cache.NewSharedIndexInformer(cmListWatch, &v1.VirtualMachineInstance{}, time.Second, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
go cmInformer.Run(stopChan)
cache.WaitForCacheSync(stopChan, cmInformer.HasSynced)
_, err := NewClusterConfig(cmInformer.GetStore()).GetImagePullPolicy()
Expect(err).To(HaveOccurred())
})
})
func MakeFakeConfigMapWatcher(configMaps []kubev1.ConfigMap) *cache.ListWatch {
cmListWatch := &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return &kubev1.ConfigMapList{Items: configMaps}, nil
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
fakeWatch := watch.NewFake()
for _, cfgMap := range configMaps {
fakeWatch.Add(&cfgMap)
}
return watch.NewFake(), nil
},
}
return cmListWatch
}
View
@@ -36,6 +36,8 @@ import (
"kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/log"
cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/datavolumecontroller/v1alpha1"
)
const (
@@ -121,6 +123,10 @@ func PodKey(pod *k8sv1.Pod) string {
return fmt.Sprintf("%v/%v", pod.Namespace, pod.Name)
}
func DataVolumeKey(dataVolume *cdiv1.DataVolume) string {
return fmt.Sprintf("%v/%v", dataVolume.Namespace, dataVolume.Name)
}
func VirtualMachineKeys(vmis []*v1.VirtualMachineInstance) []string {
keys := []string{}
for _, vmi := range vmis {
View
@@ -34,6 +34,8 @@ import (
virtv1 "kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/kubecli"
"kubevirt.io/kubevirt/pkg/log"
cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/datavolumecontroller/v1alpha1"
)
// GetControllerOf returns the controllerRef if controllee has a controller,
@@ -224,6 +226,49 @@ func (m *VirtualMachineControllerRefManager) ClaimVirtualMachines(vmis []*virtv1
return claimed, utilerrors.NewAggregate(errlist)
}
// ClaimDataVolume tries to take ownership of a list of DataVolumes.
//
// It will reconcile the following:
// * Adopt orphans if the selector matches.
// * Release owned objects if the selector no longer matches.
//
// Optional: If one or more filters are specified, a DataVolume will only be claimed if
// all filters return true.
//
// A non-nil error is returned if some form of reconciliation was attempted and
// failed. Usually, controllers should try again later in case reconciliation
// is still needed.
//
// If the error is nil, either the reconciliation succeeded, or no
// reconciliation was necessary. The list of DataVolumes that you now own is returned.
func (m *VirtualMachineControllerRefManager) ClaimMatchedDataVolumes(dataVolumes []*cdiv1.DataVolume, filters ...func(machine *cdiv1.DataVolume) bool) ([]*cdiv1.DataVolume, error) {
var claimed []*cdiv1.DataVolume
var errlist []error
match := func(obj metav1.Object) bool {
return true
}
adopt := func(obj metav1.Object) error {
return m.AdoptDataVolume(obj.(*cdiv1.DataVolume))
}
release := func(obj metav1.Object) error {
return m.ReleaseDataVolume(obj.(*cdiv1.DataVolume))
}
for _, dataVolume := range dataVolumes {
ok, err := m.ClaimObject(dataVolume, match, adopt, release)
if err != nil {
errlist = append(errlist, err)
continue
}
if ok {
claimed = append(claimed, dataVolume)
}
}
return claimed, utilerrors.NewAggregate(errlist)
}
// ClaimVirtualMachineByName tries to take ownership of a VirtualMachineInstance.
//
// It will reconcile the following:
@@ -313,8 +358,52 @@ func (m *VirtualMachineControllerRefManager) ReleaseVirtualMachine(vmi *virtv1.V
return err
}
// AdoptDataVolume sends a patch to take control of the dataVolume. It returns the error if
// the patching fails.
func (m *VirtualMachineControllerRefManager) AdoptDataVolume(dataVolume *cdiv1.DataVolume) error {
if err := m.CanAdopt(); err != nil {
return fmt.Errorf("can't adopt DataVolume %v/%v (%v): %v", dataVolume.Namespace, dataVolume.Name, dataVolume.UID, err)
}
// Note that ValidateOwnerReferences() will reject this patch if another
// OwnerReference exists with controller=true.
addControllerPatch := fmt.Sprintf(
`{"metadata":{"ownerReferences":[{"apiVersion":"%s","kind":"%s","name":"%s","uid":"%s","controller":true,"blockOwnerDeletion":true}],"uid":"%s"}}`,
m.controllerKind.GroupVersion(), m.controllerKind.Kind,
m.Controller.GetName(), m.Controller.GetUID(), dataVolume.UID)
return m.virtualMachineControl.PatchVirtualMachine(dataVolume.Namespace, dataVolume.Name, []byte(addControllerPatch))
}
// ReleaseDataVolume sends a patch to free the dataVolume from the control of the controller.
// It returns the error if the patching fails. 404 and 422 errors are ignored.
func (m *VirtualMachineControllerRefManager) ReleaseDataVolume(dataVolume *cdiv1.DataVolume) error {
log.Log.V(2).Object(dataVolume).Infof("patching dataVolume to remove its controllerRef to %s/%s:%s",
m.controllerKind.GroupVersion(), m.controllerKind.Kind, m.Controller.GetName())
// TODO CRDs don't support strategic merge, therefore replace the onwerReferences list with a merge patch
deleteOwnerRefPatch := fmt.Sprint(`{"metadata":{"ownerReferences":[]}}`)
err := m.virtualMachineControl.PatchDataVolume(dataVolume.Namespace, dataVolume.Name, []byte(deleteOwnerRefPatch))
if err != nil {
if errors.IsNotFound(err) {
// If no longer exists, ignore it.
return nil
}
if errors.IsInvalid(err) {
// Invalid error will be returned in two cases: 1. the dataVolume
// has no owner reference, 2. the uid of the dataVolume doesn't
// match, which means the dataVolume is deleted and then recreated.
// In both cases, the error can be ignored.
// TODO: If the dataVolume has owner references, but none of them
// has the owner.UID, server will silently ignore the patch.
// Investigate why.
return nil
}
}
return err
}
type VirtualMachineControlInterface interface {
PatchVirtualMachine(namespace, name string, data []byte) error
PatchDataVolume(namespace, name string, data []byte) error
}
type RealVirtualMachineControl struct {
@@ -327,6 +416,12 @@ func (r RealVirtualMachineControl) PatchVirtualMachine(namespace, name string, d
return err
}
func (r RealVirtualMachineControl) PatchDataVolume(namespace, name string, data []byte) error {
// TODO should be a strategic merge patch, but not possible until https://github.com/kubernetes/kubernetes/issues/56348 is resolved
_, err := r.Clientset.CdiClient().CdiV1alpha1().DataVolumes(namespace).Patch(name, types.MergePatchType, data)
return err
}
// RecheckDeletionTimestamp returns a CanAdopt() function to recheck deletion.
//
// The CanAdopt() function calls getObject() to fetch the latest value,
View
@@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/datavolumecontroller/v1alpha1"
virtv1 "kubevirt.io/kubevirt/pkg/api/v1"
)
@@ -186,6 +187,120 @@ func TestClaimVirtualMachine(t *testing.T) {
}
}
func newDataVolume(name string, owner metav1.Object) *cdiv1.DataVolume {
dataVolume := &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: metav1.NamespaceDefault,
},
}
if owner != nil {
dataVolume.OwnerReferences = []metav1.OwnerReference{*newControllerRef(owner)}
}
return dataVolume
}
func TestClaimDataVolume(t *testing.T) {
controllerKind := schema.GroupVersionKind{}
type test struct {
name string
manager *VirtualMachineControllerRefManager
datavolumes []*cdiv1.DataVolume
filters []func(*cdiv1.DataVolume) bool
claimed []*cdiv1.DataVolume
released []*cdiv1.DataVolume
expectError bool
}
var tests = []test{
func() test {
controller := v1.ReplicationController{}
controller.UID = types.UID(controllerUID)
now := metav1.Now()
controller.DeletionTimestamp = &now
return test{
name: "Controller marked for deletion can not claim datavolumes",
manager: NewVirtualMachineControllerRefManager(&FakeVirtualMachineControl{},
&controller,
productionLabelSelector,
controllerKind,
func() error { return nil }),
datavolumes: []*cdiv1.DataVolume{newDataVolume("datavolume1", nil), newDataVolume("datavolume2", nil)},
claimed: nil,
}
}(),
func() test {
controller := v1.ReplicationController{}
controller.UID = types.UID(controllerUID)
now := metav1.Now()
controller.DeletionTimestamp = &now
return test{
name: "Controller marked for deletion can not claim new datavolumes",
manager: NewVirtualMachineControllerRefManager(&FakeVirtualMachineControl{},
&controller,
productionLabelSelector,
controllerKind,
func() error { return nil }),
datavolumes: []*cdiv1.DataVolume{newDataVolume("datavolume1", &controller), newDataVolume("datavolume2", nil)},
claimed: []*cdiv1.DataVolume{newDataVolume("datavolume1", &controller)},
}
}(),
func() test {
controller := v1.ReplicationController{}
controller2 := v1.ReplicationController{}
controller.UID = types.UID(controllerUID)
controller2.UID = types.UID("AAAAA")
return test{
name: "Controller can not claim datavolumes owned by another controller",
manager: NewVirtualMachineControllerRefManager(&FakeVirtualMachineControl{},
&controller,
productionLabelSelector,
controllerKind,
func() error { return nil }),
datavolumes: []*cdiv1.DataVolume{newDataVolume("datavolume1", &controller), newDataVolume("datavolume2", &controller2)},
claimed: []*cdiv1.DataVolume{newDataVolume("datavolume1", &controller)},
}
}(),
func() test {
controller := v1.ReplicationController{}
controller.UID = types.UID(controllerUID)
datavolumeToDelete1 := newDataVolume("datavolume1", &controller)
datavolumeToDelete2 := newDataVolume("datavolume2", nil)
now := metav1.Now()
datavolumeToDelete1.DeletionTimestamp = &now
datavolumeToDelete2.DeletionTimestamp = &now
return test{
name: "Controller does not claim orphaned datavolumes marked for deletion",
manager: NewVirtualMachineControllerRefManager(&FakeVirtualMachineControl{},
&controller,
productionLabelSelector,
controllerKind,
func() error { return nil }),
datavolumes: []*cdiv1.DataVolume{datavolumeToDelete1, datavolumeToDelete2},
claimed: []*cdiv1.DataVolume{datavolumeToDelete1},
}
}(),
}
for _, test := range tests {
claimed, err := test.manager.ClaimMatchedDataVolumes(test.datavolumes)
if test.expectError && err == nil {
t.Errorf("Test case `%s`, expected error but got nil", test.name)
} else if !reflect.DeepEqual(test.claimed, claimed) {
t.Errorf("Test case `%s`, claimed wrong datavolumes. Expected %v, got %v", test.name, datavolumeToStringSlice(test.claimed), datavolumeToStringSlice(claimed))
}
}
}
func datavolumeToStringSlice(dataVolumes []*cdiv1.DataVolume) []string {
var names []string
for _, dv := range dataVolumes {
names = append(names, dv.Name)
}
return names
}
func virtualmachineToStringSlice(virtualmachines []*virtv1.VirtualMachineInstance) []string {
var names []string
for _, virtualmachine := range virtualmachines {
@@ -212,3 +327,12 @@ func (f *FakeVirtualMachineControl) PatchVirtualMachine(namespace, name string,
}
return nil
}
func (f *FakeVirtualMachineControl) PatchDataVolume(namespace, name string, data []byte) error {
f.Lock()
defer f.Unlock()
f.Patches = append(f.Patches, data)
if f.Err != nil {
return f.Err
}
return nil
}
View
@@ -31,9 +31,13 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/datavolumecontroller/v1alpha1"
cdiv1informers "kubevirt.io/containerized-data-importer/pkg/client/informers/externalversions"
kubev1 "kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/kubecli"
"kubevirt.io/kubevirt/pkg/log"
"kubevirt.io/kubevirt/pkg/testutils"
)
const systemNamespace = "kube-system"
@@ -68,6 +72,12 @@ type KubeInformerFactory interface {
// Watches for LimitRange objects
LimitRanges() cache.SharedIndexInformer
// Watches for CDI DataVolume objects
DataVolume() cache.SharedIndexInformer
// Fake CDI DataVolume informer used when feature gate is disabled
DummyDataVolume() cache.SharedIndexInformer
}
type kubeInformerFactory struct {
@@ -175,6 +185,21 @@ func (f *kubeInformerFactory) VirtualMachine() cache.SharedIndexInformer {
})
}
func (f *kubeInformerFactory) DataVolume() cache.SharedIndexInformer {
return f.getInformer("dataVolumeInformer", func() cache.SharedIndexInformer {
cdiClient := f.clientSet.CdiClient()
cdiInformerFactory := cdiv1informers.NewSharedInformerFactory(cdiClient, f.defaultResync)
return cdiInformerFactory.Cdi().V1alpha1().DataVolumes().Informer()
})
}
func (f *kubeInformerFactory) DummyDataVolume() cache.SharedIndexInformer {
return f.getInformer("fakeDataVolumeInformer", func() cache.SharedIndexInformer {
informer, _ := testutils.NewFakeInformerFor(&cdiv1.DataVolume{})
return informer
})
}
func (f *kubeInformerFactory) ConfigMap() cache.SharedIndexInformer {
// We currently only monitor configmaps in the kube-system namespace
return f.getInformer("configMapInformer", func() cache.SharedIndexInformer {
View
@@ -0,0 +1,89 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2017, 2018 Red Hat, Inc.
*
*/
package featuregates
import (
"io/ioutil"
"os"
"strings"
"time"
k8sv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/api/errors"
"kubevirt.io/kubevirt/pkg/kubecli"
)
const featureGateEnvVar = "FEATURE_GATES"
const (
dataVolumesGate = "DataVolumes"
)
func getNamespace() string {
if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
if ns := strings.TrimSpace(string(data)); len(ns) > 0 {
return ns
}
}
return metav1.NamespaceSystem
}
func ParseFeatureGatesFromConfigMap() {
virtClient, err := kubecli.GetKubevirtClient()
if err != nil {
panic(err)
}
var cfgMap *k8sv1.ConfigMap
var curErr error
err = wait.PollImmediate(time.Second*1, time.Second*10, func() (bool, error) {
cfgMap, curErr = virtClient.CoreV1().ConfigMaps(getNamespace()).Get("kubevirt-config", metav1.GetOptions{})
if curErr != nil {
if errors.IsNotFound(curErr) {
// ignore if config map does not exist
return true, nil
}
return false, curErr
}
val, ok := cfgMap.Data["feature-gates"]
if !ok {
// no feature gates set
return true, nil
}
os.Setenv(featureGateEnvVar, val)
return true, nil
})
if err != nil {
panic(err)
}
}
func DataVolumesEnabled() bool {
return strings.Contains(os.Getenv(featureGateEnvVar), dataVolumesGate)
}
View
@@ -0,0 +1,100 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package hostdisk
import (
"fmt"
"os"
"path"
"syscall"
k8sv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/kubecli"
"kubevirt.io/kubevirt/pkg/precond"
)
const pvcBaseDir = "/var/run/kubevirt-private/vmi-disks"
func dirBytesAvailable(path string) (uint64, error) {
var stat syscall.Statfs_t
err := syscall.Statfs(path, &stat)
if err != nil {
return 0, err
}
return (stat.Bavail * uint64(stat.Bsize)), nil
}
func createSparseRaw(fullPath string, size int64) error {
offset := size - 1
f, _ := os.Create(fullPath)
defer f.Close()
_, err := f.WriteAt([]byte{0}, offset)
if err != nil {
return err
}
return nil
}
func GetPVCDiskImgPath(volumeName string) string {
return path.Join(pvcBaseDir, volumeName, "disk.img")
}
func GetPVCSize(pvcName string, namespace string, clientset kubecli.KubevirtClient) (resource.Quantity, error) {
precond.CheckNotNil(pvcName)
precond.CheckNotEmpty(namespace)
precond.CheckNotNil(clientset)
pvc, err := clientset.CoreV1().PersistentVolumeClaims(namespace).Get(pvcName, metav1.GetOptions{})
if err != nil {
return resource.Quantity{}, err
}
return pvc.Status.Capacity[k8sv1.ResourceStorage], nil
}
func CreateHostDisks(vmi *v1.VirtualMachineInstance) error {
for _, volume := range vmi.Spec.Volumes {
if hostDisk := volume.VolumeSource.HostDisk; hostDisk != nil && hostDisk.Type == v1.HostDiskExistsOrCreate && hostDisk.Path != "" {
if _, err := os.Stat(hostDisk.Path); os.IsNotExist(err) {
availableSpace, err := dirBytesAvailable(path.Dir(hostDisk.Path))
if err != nil {
return err
}
size, _ := hostDisk.Capacity.AsInt64()
if uint64(size) > availableSpace {
return fmt.Errorf("Unable to create %s with size %s - not enough space on the cluster", hostDisk.Path, hostDisk.Capacity.String())
}
err = createSparseRaw(hostDisk.Path, size)
if err != nil {
return err
}
} else if err != nil {
return err
}
}
}
return nil
}
View
@@ -0,0 +1,227 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package hostdisk
import (
"io/ioutil"
"os"
"path"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/resource"
"kubevirt.io/kubevirt/pkg/api/v1"
)
var _ = Describe("HostDisk", func() {
var tempDir string
addHostDisk := func(vmi *v1.VirtualMachineInstance, volumeName string, hostDiskType v1.HostDiskType, capacity string) {
var quantity resource.Quantity
err := os.Mkdir(path.Join(tempDir, volumeName), 0755)
if !os.IsExist(err) {
Expect(err).NotTo(HaveOccurred())
}
if capacity != "" {
quantity, err = resource.ParseQuantity(capacity)
Expect(err).NotTo(HaveOccurred())
}
vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{
Name: volumeName,
VolumeSource: v1.VolumeSource{
HostDisk: &v1.HostDisk{
Path: path.Join(tempDir, volumeName, "disk.img"),
Type: hostDiskType,
Capacity: quantity,
},
},
})
}
createTempDiskImg := func(volumeName string) os.FileInfo {
imgPath := path.Join(tempDir, volumeName, "disk.img")
err := os.Mkdir(path.Join(tempDir, volumeName), 0755)
Expect(err).NotTo(HaveOccurred())
// 67108864 = 64Mi
err = createSparseRaw(imgPath, 67108864)
Expect(err).NotTo(HaveOccurred())
file, err := os.Stat(imgPath)
Expect(err).NotTo(HaveOccurred())
return file
}
BeforeEach(func() {
var err error
tempDir, err = ioutil.TempDir("", "host-disk-images")
Expect(err).NotTo(HaveOccurred())
})
AfterEach(func() {
os.RemoveAll(tempDir)
})
Describe("HostDisk with 'Disk' type", func() {
It("Should not create a disk.img when it exists", func() {
By("Creating a disk.img before adding a HostDisk volume")
tmpDiskImg := createTempDiskImg("volume1")
By("Creating a new minimal vmi")
vmi := v1.NewMinimalVMI("fake-vmi")
By("Adding a HostDisk volume for existing disk.img")
addHostDisk(vmi, "volume1", v1.HostDiskExists, "")
By("Executing CreateHostDisks which should not create a disk.img")
err := CreateHostDisks(vmi)
Expect(err).NotTo(HaveOccurred())
// check if disk.img has the same modification time
// which means that CreateHostDisks function did not create a new disk.img
hostDiskImg, _ := os.Stat(vmi.Spec.Volumes[0].HostDisk.Path)
Expect(tmpDiskImg.ModTime()).To(Equal(hostDiskImg.ModTime()))
})
It("Should not create a disk.img when it does not exist", func() {
By("Creating a new minimal vmi")
vmi := v1.NewMinimalVMI("fake-vmi")
By("Adding a HostDisk volume")
addHostDisk(vmi, "volume1", v1.HostDiskExists, "")
By("Executing CreateHostDisks which should not create disk.img")
err := CreateHostDisks(vmi)
Expect(err).NotTo(HaveOccurred())
// disk.img should not exist
_, err = os.Stat(vmi.Spec.Volumes[0].HostDisk.Path)
Expect(true).To(Equal(os.IsNotExist(err)))
})
})
Describe("HostDisk with 'DiskOrCreate' type", func() {
Context("With multiple HostDisk volumes", func() {
Context("With non existing disk.img", func() {
It("Should create disk.img if there is enough space", func() {
By("Creating a new minimal vmi")
vmi := v1.NewMinimalVMI("fake-vmi")
By("Adding a HostDisk volumes")
addHostDisk(vmi, "volume1", v1.HostDiskExistsOrCreate, "64Mi")
addHostDisk(vmi, "volume2", v1.HostDiskExistsOrCreate, "128Mi")
addHostDisk(vmi, "volume3", v1.HostDiskExistsOrCreate, "80Mi")
By("Executing CreateHostDisks which should create disk.img")
err := CreateHostDisks(vmi)
Expect(err).NotTo(HaveOccurred())
// check if images exist and the size is adequate to requirements
img1, err := os.Stat(vmi.Spec.Volumes[0].HostDisk.Path)
Expect(err).NotTo(HaveOccurred())
Expect(img1.Size()).To(Equal(int64(67108864))) // 64Mi
img2, err := os.Stat(vmi.Spec.Volumes[1].HostDisk.Path)
Expect(err).NotTo(HaveOccurred())
Expect(img2.Size()).To(Equal(int64(134217728))) // 128Mi
img3, err := os.Stat(vmi.Spec.Volumes[2].HostDisk.Path)
Expect(err).NotTo(HaveOccurred())
Expect(img3.Size()).To(Equal(int64(83886080))) // 80Mi
})
It("Should stop creating disk images if there is not enough space and should return err", func() {
By("Creating a new minimal vmi")
vmi := v1.NewMinimalVMI("fake-vmi")
By("Adding a HostDisk volumes")
addHostDisk(vmi, "volume1", v1.HostDiskExistsOrCreate, "64Mi")
addHostDisk(vmi, "volume2", v1.HostDiskExistsOrCreate, "1E")
addHostDisk(vmi, "volume3", v1.HostDiskExistsOrCreate, "128Mi")
By("Executing CreateHostDisks func which should not create a disk.img")
err := CreateHostDisks(vmi)
Expect(err).To(HaveOccurred())
// only first disk.img should be created
// when there is not enough space anymore
// function should return err and stop creating images
img1, err := os.Stat(vmi.Spec.Volumes[0].HostDisk.Path)
Expect(err).NotTo(HaveOccurred())
Expect(img1.Size()).To(Equal(int64(67108864))) // 64Mi
_, err = os.Stat(vmi.Spec.Volumes[1].HostDisk.Path)
Expect(true).To(Equal(os.IsNotExist(err)))
_, err = os.Stat(vmi.Spec.Volumes[2].HostDisk.Path)
Expect(true).To(Equal(os.IsNotExist(err)))
})
})
})
Context("With existing disk.img", func() {
It("Should not re-create disk.img", func() {
By("Creating a disk.img before adding a HostDisk volume")
tmpDiskImg := createTempDiskImg("volume1")
By("Creating a new minimal vmi")
vmi := v1.NewMinimalVMI("fake-vmi")
By("Adding a HostDisk volume")
addHostDisk(vmi, "volume1", v1.HostDiskExistsOrCreate, "128Mi")
By("Executing CreateHostDisks which should not create a disk.img")
err := CreateHostDisks(vmi)
Expect(err).NotTo(HaveOccurred())
// check if disk.img has the same modification time
// which means that CreateHostDisks function did not create a new disk.img
capacity := vmi.Spec.Volumes[0].HostDisk.Capacity
specSize, _ := capacity.AsInt64()
hostDiskImg, _ := os.Stat(vmi.Spec.Volumes[0].HostDisk.Path)
Expect(tmpDiskImg.ModTime()).To(Equal(hostDiskImg.ModTime()))
// check if img has the same size as before
Expect(tmpDiskImg.Size()).NotTo(Equal(specSize))
Expect(tmpDiskImg.Size()).To(Equal(int64(67108864)))
})
})
})
Describe("HostDisk with unkown type", func() {
It("Should not create a disk.img", func() {
By("Creating a new minimal vmi")
vmi := v1.NewMinimalVMI("fake-vmi")
By("Adding a HostDisk volume with unknown type")
addHostDisk(vmi, "volume1", "UnknownType", "")
By("Executing CreateHostDisks which should not create a disk.img")
err := CreateHostDisks(vmi)
Expect(err).NotTo(HaveOccurred())
// disk.img should not exist
_, err = os.Stat(vmi.Spec.Volumes[0].HostDisk.Path)
Expect(true).To(Equal(os.IsNotExist(err)))
})
})
})
View
@@ -0,0 +1,13 @@
package hostdisk_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestHostDisk(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "HostDisk Suite")
}
View
@@ -40,6 +40,7 @@ import (
v1beta19 "k8s.io/client-go/kubernetes/typed/storage/v1beta1"
rest "k8s.io/client-go/rest"
versioned "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned"
v19 "kubevirt.io/kubevirt/pkg/api/v1"
)
@@ -114,6 +115,16 @@ func (_mr *_MockKubevirtClientRecorder) RestClient() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "RestClient")
}
func (_m *MockKubevirtClient) CdiClient() versioned.Interface {
ret := _m.ctrl.Call(_m, "CdiClient")
ret0, _ := ret[0].(versioned.Interface)
return ret0
}
func (_mr *_MockKubevirtClientRecorder) CdiClient() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "CdiClient")
}
func (_m *MockKubevirtClient) Discovery() discovery.DiscoveryInterface {
ret := _m.ctrl.Call(_m, "Discovery")
ret0, _ := ret[0].(discovery.DiscoveryInterface)
View
@@ -22,6 +22,7 @@ package kubecli
import (
"flag"
"os"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
@@ -31,7 +32,7 @@ import (
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"os"
cdiclient "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned"
"github.com/spf13/pflag"
@@ -69,7 +70,19 @@ func GetKubevirtSubresourceClientFromFlags(master string, kubeconfig string) (Ku
return nil, err
}
return &kubevirt{master, kubeconfig, restClient, config, coreClient}, nil
cdiClient, err := cdiclient.NewForConfig(config)
if err != nil {
return nil, err
}
return &kubevirt{
master,
kubeconfig,
restClient,
config,
cdiClient,
coreClient,
}, nil
}
// DefaultClientConfig creates a clientcmd.ClientConfig with the following hierarchy:
@@ -158,7 +171,19 @@ func GetKubevirtClientFromRESTConfig(config *rest.Config) (KubevirtClient, error
return nil, err
}
return &kubevirt{master, kubeconfig, restClient, config, coreClient}, nil
cdiClient, err := cdiclient.NewForConfig(config)
if err != nil {
return nil, err
}
return &kubevirt{
master,
kubeconfig,
restClient,
config,
cdiClient,
coreClient,
}, nil
}
func GetKubevirtClientFromFlags(master string, kubeconfig string) (KubevirtClient, error) {
View
@@ -30,10 +30,11 @@ import (
"time"
k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/apimachinery/pkg/types"
cdiclient "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned"
"kubevirt.io/kubevirt/pkg/api/v1"
)
@@ -44,6 +45,7 @@ type KubevirtClient interface {
VirtualMachine(namespace string) VirtualMachineInterface
ServerVersion() *ServerVersion
RestClient() *rest.RESTClient
CdiClient() cdiclient.Interface
kubernetes.Interface
}
@@ -52,9 +54,14 @@ type kubevirt struct {
kubeconfig string
restClient *rest.RESTClient
config *rest.Config
cdiClient *cdiclient.Clientset
*kubernetes.Clientset
}
func (k kubevirt) CdiClient() cdiclient.Interface {
return k.cdiClient
}
func (k kubevirt) RestClient() *rest.RESTClient {
return k.restClient
}
@@ -97,7 +104,7 @@ type VMIPresetInterface interface {
}
// VirtualMachineInterface provides convenience methods to work with
// offline virtual machines inside the cluster
// virtual machines inside the cluster
type VirtualMachineInterface interface {
Get(name string, options *k8smetav1.GetOptions) (*v1.VirtualMachine, error)
List(opts *k8smetav1.ListOptions) (*v1.VirtualMachineList, error)
View
@@ -36,8 +36,8 @@ func NewMinimalVM(name string) *v1.VirtualMachine {
return &v1.VirtualMachine{TypeMeta: k8smetav1.TypeMeta{APIVersion: v1.GroupVersion.String(), Kind: "VirtualMachine"}, ObjectMeta: k8smetav1.ObjectMeta{Name: name}}
}
func NewVMList(ovms ...v1.VirtualMachine) *v1.VirtualMachineList {
return &v1.VirtualMachineList{TypeMeta: k8smetav1.TypeMeta{APIVersion: v1.GroupVersion.String(), Kind: "VirtualMachineList"}, Items: ovms}
func NewVMList(vms ...v1.VirtualMachine) *v1.VirtualMachineList {
return &v1.VirtualMachineList{TypeMeta: k8smetav1.TypeMeta{APIVersion: v1.GroupVersion.String(), Kind: "VirtualMachineList"}, Items: vms}
}
func NewVirtualMachineInstanceReplicaSetList(rss ...v1.VirtualMachineInstanceReplicaSet) *v1.VirtualMachineInstanceReplicaSetList {
View
@@ -44,50 +44,50 @@ type vm struct {
}
// Create new VirtualMachine in the cluster to specified namespace
func (o *vm) Create(offlinevm *v1.VirtualMachine) (*v1.VirtualMachine, error) {
newOvmi := &v1.VirtualMachine{}
func (o *vm) Create(vm *v1.VirtualMachine) (*v1.VirtualMachine, error) {
newVm := &v1.VirtualMachine{}
err := o.restClient.Post().
Resource(o.resource).
Namespace(o.namespace).
Body(offlinevm).
Body(vm).
Do().
Into(newOvmi)
Into(newVm)
newOvmi.SetGroupVersionKind(v1.VirtualMachineGroupVersionKind)
newVm.SetGroupVersionKind(v1.VirtualMachineGroupVersionKind)
return newOvmi, err
return newVm, err
}
// Get the OfflineVirtual machine from the cluster by its name and namespace
func (o *vm) Get(name string, options *k8smetav1.GetOptions) (*v1.VirtualMachine, error) {
newOvm := &v1.VirtualMachine{}
newVm := &v1.VirtualMachine{}
err := o.restClient.Get().
Resource(o.resource).
Namespace(o.namespace).
Name(name).
VersionedParams(options, scheme.ParameterCodec).
Do().
Into(newOvm)
Into(newVm)
newOvm.SetGroupVersionKind(v1.VirtualMachineGroupVersionKind)
newVm.SetGroupVersionKind(v1.VirtualMachineGroupVersionKind)
return newOvm, err
return newVm, err
}
// Update the VirtualMachine instance in the cluster in given namespace
func (o *vm) Update(offlinevm *v1.VirtualMachine) (*v1.VirtualMachine, error) {
updatedOvmi := &v1.VirtualMachine{}
func (o *vm) Update(vm *v1.VirtualMachine) (*v1.VirtualMachine, error) {
updatedVm := &v1.VirtualMachine{}
err := o.restClient.Put().
Resource(o.resource).
Namespace(o.namespace).
Name(offlinevm.Name).
Body(offlinevm).
Name(vm.Name).
Body(vm).
Do().
Into(updatedOvmi)
Into(updatedVm)
updatedOvmi.SetGroupVersionKind(v1.VirtualMachineGroupVersionKind)
updatedVm.SetGroupVersionKind(v1.VirtualMachineGroupVersionKind)
return updatedOvmi, err
return updatedVm, err
}
// Delete the defined VirtualMachine in the cluster in defined namespace
@@ -105,19 +105,19 @@ func (o *vm) Delete(name string, options *k8smetav1.DeleteOptions) error {
// List all VirtualMachines in given namespace
func (o *vm) List(options *k8smetav1.ListOptions) (*v1.VirtualMachineList, error) {
newOvmList := &v1.VirtualMachineList{}
newVmList := &v1.VirtualMachineList{}
err := o.restClient.Get().
Resource(o.resource).
Namespace(o.namespace).
VersionedParams(options, scheme.ParameterCodec).
Do().
Into(newOvmList)
Into(newVmList)
for _, vm := range newOvmList.Items {
for _, vm := range newVmList.Items {
vm.SetGroupVersionKind(v1.VirtualMachineGroupVersionKind)
}
return newOvmList, err
return newVmList, err
}
func (v *vm) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.VirtualMachine, err error) {
View
@@ -37,6 +37,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/util/subresources"
)
const (
@@ -178,6 +179,7 @@ func roundTripperFromConfig(config *rest.Config, callback RoundTripCallback) (ht
TLSClientConfig: tlsConfig,
WriteBufferSize: WebsocketMessageBufferSize,
ReadBufferSize: WebsocketMessageBufferSize,
Subprotocols: []string{subresources.PlainStreamProtocolName},
}
// Create a roundtripper which will pass in the final underlying websocket connection to a callback
View
@@ -12,6 +12,8 @@ import (
"kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/virt-launcher/virtwrap/api"
cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/datavolumecontroller/v1alpha1"
)
/*
@@ -156,3 +158,33 @@ func NewDomainFeeder(queue *MockWorkQueue, source *framework.FakeControllerSourc
Source: source,
}
}
type DataVolumeFeeder struct {
MockQueue *MockWorkQueue
Source *framework.FakeControllerSource
}
func (v *DataVolumeFeeder) Add(dataVolume *cdiv1.DataVolume) {
v.MockQueue.ExpectAdds(1)
v.Source.Add(dataVolume)
v.MockQueue.Wait()
}
func (v *DataVolumeFeeder) Modify(dataVolume *cdiv1.DataVolume) {
v.MockQueue.ExpectAdds(1)
v.Source.Modify(dataVolume)
v.MockQueue.Wait()
}
func (v *DataVolumeFeeder) Delete(dataVolume *cdiv1.DataVolume) {
v.MockQueue.ExpectAdds(1)
v.Source.Delete(dataVolume)
v.MockQueue.Wait()
}
func NewDataVolumeFeeder(queue *MockWorkQueue, source *framework.FakeControllerSource) *DataVolumeFeeder {
return &DataVolumeFeeder{
MockQueue: queue,
Source: source,
}
}
View
@@ -0,0 +1,40 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package util
import (
"fmt"
"regexp"
)
const PCI_ADDRESS_PATTERN = `^([\da-fA-F]{4}):([\da-fA-F]{2}):([\da-fA-F]{2}).([0-7]{1})$`
// ParsePciAddress returns an array of PCI DBSF fields (domain, bus, slot, function)
func ParsePciAddress(pciAddress string) ([]string, error) {
pciAddrRegx, err := regexp.Compile(PCI_ADDRESS_PATTERN)
if err != nil {
return nil, fmt.Errorf("failed to compile pci address pattern, %v", err)
}
res := pciAddrRegx.FindStringSubmatch(pciAddress)
if len(res) == 0 {
return nil, fmt.Errorf("failed to parse pci address %s", pciAddress)
}
return res[1:], nil
}
View
@@ -0,0 +1,25 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 The KubeVirt Authors
*
*/
package subresources
// PlainStreamProtocolName is a subprotocol which indicates a plain websocket stream.
// Mostly useful for browser connections which need to use the websocket subprotocol
// field to pass credentials. As a consequence they need to get a subprotocol back.
const PlainStreamProtocolName = "plain.kubevirt.io"
View
@@ -46,14 +46,18 @@ import (
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
"kubevirt.io/kubevirt/pkg/api/v1"
featuregates "kubevirt.io/kubevirt/pkg/feature-gates"
"kubevirt.io/kubevirt/pkg/healthz"
"kubevirt.io/kubevirt/pkg/kubecli"
"kubevirt.io/kubevirt/pkg/log"
"kubevirt.io/kubevirt/pkg/rest/filter"
"kubevirt.io/kubevirt/pkg/service"
"kubevirt.io/kubevirt/pkg/version"
"kubevirt.io/kubevirt/pkg/virt-api/rest"
"kubevirt.io/kubevirt/pkg/virt-api/validating-webhook"
"kubevirt.io/kubevirt/pkg/virt-api/webhooks"
"kubevirt.io/kubevirt/pkg/virt-api/webhooks/mutating-webhook"
"kubevirt.io/kubevirt/pkg/virt-api/webhooks/validating-webhook"
)
const (
@@ -67,14 +71,18 @@ const (
virtApiCertSecretName = "kubevirt-virt-api-certs"
virtWebhookValidator = "virt-api-validator"
virtWebhookMutator = "virt-api-mutator"
virtApiServiceName = "virt-api"
vmiiValidatePath = "/virtualmachineinstances-validate"
vmiValidatePath = "/virtualmachines-validate"
vmiCreateValidatePath = "/virtualmachineinstances-validate-create"
vmiUpdateValidatePath = "/virtualmachineinstances-validate-update"
vmValidatePath = "/virtualmachines-validate"
vmirsValidatePath = "/virtualmachinereplicaset-validate"
vmipresetValidatePath = "/vmipreset-validate"
vmiMutatePath = "/virtualmachineinstances-mutate"
certBytesValue = "cert-bytes"
keyBytesValue = "key-bytes"
signingCertBytesValue = "signing-cert-bytes"
@@ -115,6 +123,8 @@ func NewVirtApi() VirtApi {
}
func (app *virtAPIApp) Execute() {
featuregates.ParseFeatureGatesFromConfigMap()
virtCli, err := kubecli.GetKubevirtClient()
if err != nil {
panic(err)
@@ -259,6 +269,7 @@ func (app *virtAPIApp) composeSubresources(ctx context.Context) {
list.Kind = "APIResourceList"
list.GroupVersion = v1.SubresourceGroupVersion.Group + "/" + v1.SubresourceGroupVersion.Version
list.APIVersion = v1.SubresourceGroupVersion.Version
list.APIResources = []metav1.APIResource{}
response.WriteAsJson(list)
}).
@@ -509,10 +520,23 @@ func (app *virtAPIApp) getSelfSignedCert() error {
}
func (app *virtAPIApp) createWebhook() error {
err := app.createValidatingWebhook()
if err != nil {
return err
}
err = app.createMutatingWebhook()
if err != nil {
return err
}
return nil
}
func (app *virtAPIApp) createValidatingWebhook() error {
namespace := getNamespace()
registerWebhook := false
vmiPath := vmiiValidatePath
vmPath := vmiValidatePath
vmiPathCreate := vmiCreateValidatePath
vmiPathUpdate := vmiUpdateValidatePath
vmPath := vmValidatePath
vmirsPath := vmirsValidatePath
vmipresetPath := vmipresetValidatePath
@@ -527,9 +551,11 @@ func (app *virtAPIApp) createWebhook() error {
webHooks := []admissionregistrationv1beta1.Webhook{
{
Name: "virtualmachineinstances-validator.kubevirt.io",
Name: "virtualmachineinstances-create-validator.kubevirt.io",
Rules: []admissionregistrationv1beta1.RuleWithOperations{{
Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.Create},
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Create,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{v1.GroupName},
APIVersions: []string{v1.VirtualMachineInstanceGroupVersionKind.Version},
@@ -540,15 +566,39 @@ func (app *virtAPIApp) createWebhook() error {
Service: &admissionregistrationv1beta1.ServiceReference{
Namespace: namespace,
Name: virtApiServiceName,
Path: &vmiPath,
Path: &vmiPathCreate,
},
CABundle: app.signingCertBytes,
},
},
{
Name: "virtualmachineinstances-update-validator.kubevirt.io",
Rules: []admissionregistrationv1beta1.RuleWithOperations{{
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Update,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{v1.GroupName},
APIVersions: []string{v1.VirtualMachineInstanceGroupVersionKind.Version},
Resources: []string{"virtualmachineinstances"},
},
}},
ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
Service: &admissionregistrationv1beta1.ServiceReference{
Namespace: namespace,
Name: virtApiServiceName,
Path: &vmiPathUpdate,
},
CABundle: app.signingCertBytes,
},
},
{
Name: "virtualmachine-validator.kubevirt.io",
Rules: []admissionregistrationv1beta1.RuleWithOperations{{
Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.Create},
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Create,
admissionregistrationv1beta1.Update,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{v1.GroupName},
APIVersions: []string{v1.VirtualMachineGroupVersionKind.Version},
@@ -567,7 +617,10 @@ func (app *virtAPIApp) createWebhook() error {
{
Name: "virtualmachinereplicaset-validator.kubevirt.io",
Rules: []admissionregistrationv1beta1.RuleWithOperations{{
Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.Create},
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Create,
admissionregistrationv1beta1.Update,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{v1.GroupName},
APIVersions: []string{v1.VirtualMachineInstanceReplicaSetGroupVersionKind.Version},
@@ -586,7 +639,10 @@ func (app *virtAPIApp) createWebhook() error {
{
Name: "virtualmachinepreset-validator.kubevirt.io",
Rules: []admissionregistrationv1beta1.RuleWithOperations{{
Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.Create},
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Create,
admissionregistrationv1beta1.Update,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{v1.GroupName},
APIVersions: []string{v1.VirtualMachineInstancePresetGroupVersionKind.Version},
@@ -634,10 +690,13 @@ func (app *virtAPIApp) createWebhook() error {
}
}
http.HandleFunc(vmiiValidatePath, func(w http.ResponseWriter, r *http.Request) {
validating_webhook.ServeVMIs(w, r)
http.HandleFunc(vmiCreateValidatePath, func(w http.ResponseWriter, r *http.Request) {
validating_webhook.ServeVMICreate(w, r)
})
http.HandleFunc(vmiUpdateValidatePath, func(w http.ResponseWriter, r *http.Request) {
validating_webhook.ServeVMIUpdate(w, r)
})
http.HandleFunc(vmiValidatePath, func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc(vmValidatePath, func(w http.ResponseWriter, r *http.Request) {
validating_webhook.ServeVMs(w, r)
})
http.HandleFunc(vmirsValidatePath, func(w http.ResponseWriter, r *http.Request) {
@@ -650,6 +709,80 @@ func (app *virtAPIApp) createWebhook() error {
return nil
}
func (app *virtAPIApp) createMutatingWebhook() error {
namespace := getNamespace()
registerWebhook := false
vmiPath := vmiMutatePath
webhookRegistration, err := app.virtCli.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Get(virtWebhookMutator, metav1.GetOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
registerWebhook = true
} else {
return err
}
}
webHooks := []admissionregistrationv1beta1.Webhook{
{
Name: "virtualmachineinstances-mutator.kubevirt.io",
Rules: []admissionregistrationv1beta1.RuleWithOperations{{
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Create,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{v1.GroupName},
APIVersions: []string{v1.VirtualMachineInstanceGroupVersionKind.Version},
Resources: []string{"virtualmachineinstances"},
},
}},
ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
Service: &admissionregistrationv1beta1.ServiceReference{
Namespace: namespace,
Name: virtApiServiceName,
Path: &vmiPath,
},
CABundle: app.signingCertBytes,
},
},
}
if registerWebhook {
_, err := app.virtCli.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionregistrationv1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: virtWebhookMutator,
Labels: map[string]string{
v1.AppLabel: virtWebhookMutator,
},
},
Webhooks: webHooks,
})
if err != nil {
return err
}
} else {
for _, webhook := range webhookRegistration.Webhooks {
if webhook.ClientConfig.Service != nil && webhook.ClientConfig.Service.Namespace != namespace {
return fmt.Errorf("MutatingAdmissionWebhook [%s] is already registered using services endpoints in a different namespace. Existing webhook registration must be deleted before virt-api can proceed.", virtWebhookMutator)
}
}
// update registered webhook with our data
webhookRegistration.Webhooks = webHooks
_, err := app.virtCli.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Update(webhookRegistration)
if err != nil {
return err
}
}
http.HandleFunc(vmiMutatePath, func(w http.ResponseWriter, r *http.Request) {
mutating_webhook.ServeVMIs(w, r)
})
return nil
}
func (app *virtAPIApp) createSubresourceApiservice() error {
namespace := getNamespace()
config, err := kubecli.GetConfig()
@@ -795,7 +928,6 @@ func (app *virtAPIApp) startTLS() error {
}
func (app *virtAPIApp) Run() {
// get client Cert
err := app.getClientCert()
if err != nil {
@@ -814,6 +946,15 @@ func (app *virtAPIApp) Run() {
panic(err)
}
// Run informers for webhooks usage
webhookInformers := webhooks.GetInformers()
stopChan := make(chan struct{}, 1)
defer close(stopChan)
go webhookInformers.VMIInformer.Run(stopChan)
go webhookInformers.VMIPresetInformer.Run(stopChan)
go webhookInformers.NamespaceLimitsInformer.Run(stopChan)
// Verify/create webhook endpoint.
err = app.createWebhook()
if err != nil {
View
@@ -41,6 +41,7 @@ import (
"kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/kubecli"
"kubevirt.io/kubevirt/pkg/log"
"kubevirt.io/kubevirt/pkg/util/subresources"
)
type SubresourceAPIApp struct {
@@ -62,6 +63,7 @@ func (app *SubresourceAPIApp) requestHandler(request *restful.Request, response
CheckOrigin: func(_ *http.Request) bool {
return true
},
Subprotocols: []string{subresources.PlainStreamProtocolName},
}
clientSocket, err := upgrader.Upgrade(response.ResponseWriter, request.Request, nil)
View
@@ -31,6 +31,7 @@ import (
"k8s.io/client-go/tools/cache"
"kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/config"
"kubevirt.io/kubevirt/pkg/kubecli"
"kubevirt.io/kubevirt/pkg/log"
"kubevirt.io/kubevirt/pkg/virt-controller/services"
@@ -53,7 +54,7 @@ var _ = Describe("VirtualMachineInstance Subresources", func() {
vmi := v1.NewMinimalVMI("testvmi")
vmi.Status.Phase = v1.Running
vmi.ObjectMeta.SetUID(uuid.NewUUID())
templateService := services.NewTemplateService("whatever", "whatever", "whatever", configCache)
templateService := services.NewTemplateService("whatever", "whatever", "whatever", config.NewClusterConfig(configCache))
pod, err := templateService.RenderLaunchManifest(vmi)
Expect(err).ToNot(HaveOccurred())
View
@@ -0,0 +1,151 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package mutating_webhook
import (
"encoding/json"
"fmt"
"net/http"
v1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"kubevirt.io/kubevirt/pkg/api/v1"
kubev1 "kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/log"
"kubevirt.io/kubevirt/pkg/virt-api/webhooks"
)
type patchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
type mutateFunc func(*v1beta1.AdmissionReview) *v1beta1.AdmissionResponse
func serve(resp http.ResponseWriter, req *http.Request, mutate mutateFunc) {
response := v1beta1.AdmissionReview{}
review, err := webhooks.GetAdmissionReview(req)
if err != nil {
resp.WriteHeader(http.StatusBadRequest)
return
}
reviewResponse := mutate(review)
if reviewResponse != nil {
response.Response = reviewResponse
response.Response.UID = review.Request.UID
}
// reset the Object and OldObject, they are not needed in a response.
review.Request.Object = runtime.RawExtension{}
review.Request.OldObject = runtime.RawExtension{}
responseBytes, err := json.Marshal(response)
if err != nil {
log.Log.Reason(err).Errorf("failed json encode webhook response")
resp.WriteHeader(http.StatusBadRequest)
return
}
if _, err := resp.Write(responseBytes); err != nil {
log.Log.Reason(err).Errorf("failed to write webhook response")
resp.WriteHeader(http.StatusBadRequest)
return
}
resp.WriteHeader(http.StatusOK)
}
func mutateVMIs(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
vmiResource := metav1.GroupVersionResource{
Group: v1.VirtualMachineInstanceGroupVersionKind.Group,
Version: v1.VirtualMachineInstanceGroupVersionKind.Version,
Resource: "virtualmachineinstances",
}
if ar.Request.Resource != vmiResource {
err := fmt.Errorf("expect resource to be '%s'", vmiResource.Resource)
return webhooks.ToAdmissionResponseError(err)
}
raw := ar.Request.Object.Raw
vmi := v1.VirtualMachineInstance{}
err := json.Unmarshal(raw, &vmi)
if err != nil {
return webhooks.ToAdmissionResponseError(err)
}
informers := webhooks.GetInformers()
// Apply presets
err = applyPresets(&vmi, informers.VMIPresetInformer)
if err != nil {
return &v1beta1.AdmissionResponse{
Result: &metav1.Status{
Message: err.Error(),
Code: http.StatusUnprocessableEntity,
},
}
}
// Apply namespace limits
applyNamespaceLimitRangeValues(&vmi, informers.NamespaceLimitsInformer)
// Set VMI defaults
log.Log.Object(&vmi).V(4).Info("Apply defaults")
kubev1.SetObjectDefaults_VirtualMachineInstance(&vmi)
// Add foreground finalizer
vmi.Finalizers = append(vmi.Finalizers, v1.VirtualMachineInstanceFinalizer)
var patch []patchOperation
var value interface{}
value = vmi.Spec
patch = append(patch, patchOperation{
Op: "replace",
Path: "/spec",
Value: value,
})
value = vmi.ObjectMeta
patch = append(patch, patchOperation{
Op: "replace",
Path: "/metadata",
Value: value,
})
patchBytes, err := json.Marshal(patch)
if err != nil {
return webhooks.ToAdmissionResponseError(err)
}
jsonPatchType := v1beta1.PatchTypeJSONPatch
return &v1beta1.AdmissionResponse{
Allowed: true,
Patch: patchBytes,
PatchType: &jsonPatchType,
}
}
func ServeVMIs(resp http.ResponseWriter, req *http.Request) {
serve(resp, req, mutateVMIs)
}
View
@@ -0,0 +1,13 @@
package mutating_webhook_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestMutatingWebhook(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "MutatingWebhook Suite")
}
View
@@ -0,0 +1,157 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package mutating_webhook
import (
"encoding/json"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
v1beta1 "k8s.io/api/admission/v1beta1"
k8sv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
v1 "kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/testutils"
"kubevirt.io/kubevirt/pkg/virt-api/webhooks"
)
var _ = Describe("Mutating Webhook", func() {
Context("with VirtualMachineInstance admission review", func() {
var vmi *v1.VirtualMachineInstance
var preset *v1.VirtualMachineInstancePreset
var presetInformer cache.SharedIndexInformer
var namespaceLimit *k8sv1.LimitRange
var namespaceLimitInformer cache.SharedIndexInformer
memory, _ := resource.ParseQuantity("64M")
limitMemory, _ := resource.ParseQuantity("128M")
getVMISpecMetaFromResponse := func() (*v1.VirtualMachineInstanceSpec, *k8sv1.ObjectMeta) {
vmiBytes, err := json.Marshal(vmi)
Expect(err).ToNot(HaveOccurred())
By("Creating the test admissions review from the VMI")
ar := &v1beta1.AdmissionReview{
Request: &v1beta1.AdmissionRequest{
Resource: k8smetav1.GroupVersionResource{Group: v1.VirtualMachineInstanceGroupVersionKind.Group, Version: v1.VirtualMachineInstanceGroupVersionKind.Version, Resource: "virtualmachineinstances"},
Object: runtime.RawExtension{
Raw: vmiBytes,
},
},
}
By("Mutating the VMI")
resp := mutateVMIs(ar)
Expect(resp.Allowed).To(Equal(true))
By("Getting the VMI spec from the response")
vmiSpec := &v1.VirtualMachineInstanceSpec{}
vmiMeta := &k8sv1.ObjectMeta{}
patch := []patchOperation{
{Value: vmiSpec},
{Value: vmiMeta},
}
err = json.Unmarshal(resp.Patch, &patch)
Expect(err).ToNot(HaveOccurred())
Expect(patch).NotTo(BeEmpty())
return vmiSpec, vmiMeta
}
BeforeEach(func() {
vmi = &v1.VirtualMachineInstance{
ObjectMeta: k8smetav1.ObjectMeta{
Labels: map[string]string{"test": "test"},
},
Spec: v1.VirtualMachineInstanceSpec{
Domain: v1.DomainSpec{
Resources: v1.ResourceRequirements{
Requests: k8sv1.ResourceList{
"memory": memory,
},
},
},
},
}
selector := k8smetav1.LabelSelector{MatchLabels: map[string]string{"test": "test"}}
preset = &v1.VirtualMachineInstancePreset{
ObjectMeta: k8smetav1.ObjectMeta{
Name: "test-preset",
},
Spec: v1.VirtualMachineInstancePresetSpec{
Domain: &v1.DomainSpec{
CPU: &v1.CPU{Cores: 4},
},
Selector: selector,
},
}
presetInformer, _ = testutils.NewFakeInformerFor(&v1.VirtualMachineInstancePreset{})
presetInformer.GetIndexer().Add(preset)
namespaceLimit = &k8sv1.LimitRange{
Spec: k8sv1.LimitRangeSpec{
Limits: []k8sv1.LimitRangeItem{
{
Type: k8sv1.LimitTypeContainer,
Default: k8sv1.ResourceList{
k8sv1.ResourceMemory: limitMemory,
},
},
},
},
}
namespaceLimitInformer, _ = testutils.NewFakeInformerFor(&k8sv1.LimitRange{})
namespaceLimitInformer.GetIndexer().Add(namespaceLimit)
webhooks.SetInformers(
&webhooks.Informers{
VMIPresetInformer: presetInformer,
NamespaceLimitsInformer: namespaceLimitInformer,
},
)
})
It("should apply presets on VMI create", func() {
vmiSpec, _ := getVMISpecMetaFromResponse()
Expect(vmiSpec.Domain.CPU).ToNot(BeNil())
Expect(vmiSpec.Domain.CPU.Cores).To(Equal(uint32(4)))
})
It("should apply namespace limit ranges on VMI create", func() {
vmiSpec, _ := getVMISpecMetaFromResponse()
Expect(vmiSpec.Domain.Resources.Limits.Memory().String()).To(Equal("128M"))
})
It("should apply defaults on VMI create", func() {
vmiSpec, _ := getVMISpecMetaFromResponse()
Expect(vmiSpec.Domain.Machine.Type).To(Equal("q35"))
})
It("should apply foreground finalizer on VMI create", func() {
_, vmiMeta := getVMISpecMetaFromResponse()
Expect(vmiMeta.Finalizers).To(ContainElement(v1.VirtualMachineInstanceFinalizer))
})
})
})
View
@@ -0,0 +1,77 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package mutating_webhook
import (
k8sv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/client-go/tools/cache"
kubev1 "kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/log"
)
func applyNamespaceLimitRangeValues(vmi *kubev1.VirtualMachineInstance, limitrangeInformer cache.SharedIndexInformer) {
isMemoryFieldExist := func(resource k8sv1.ResourceList) bool {
_, ok := resource[k8sv1.ResourceMemory]
return ok
}
vmiResources := vmi.Spec.Domain.Resources
// Copy namespace memory limits (if exists) to the VM spec
if vmiResources.Limits == nil || !isMemoryFieldExist(vmiResources.Limits) {
namespaceMemLimit, err := getNamespaceLimits(vmi.Namespace, limitrangeInformer)
if err == nil && !namespaceMemLimit.IsZero() {
if vmiResources.Limits == nil {
vmi.Spec.Domain.Resources.Limits = make(k8sv1.ResourceList)
}
log.Log.Object(vmi).V(4).Info("Apply namespace limits")
vmi.Spec.Domain.Resources.Limits[k8sv1.ResourceMemory] = *namespaceMemLimit
}
}
}
func getNamespaceLimits(namespace string, limitrangeInformer cache.SharedIndexInformer) (*resource.Quantity, error) {
finalLimit := &resource.Quantity{Format: resource.BinarySI}
// there can be multiple LimitRange values set for the same resource in
// a namespace, we need to find the minimal
limits, err := limitrangeInformer.GetIndexer().ByIndex(cache.NamespaceIndex, namespace)
if err != nil {
return nil, err
}
for _, limit := range limits {
for _, val := range limit.(*k8sv1.LimitRange).Spec.Limits {
mem := val.Default.Memory()
if val.Type == k8sv1.LimitTypeContainer {
if !mem.IsZero() {
if finalLimit.IsZero() != (mem.Cmp(*finalLimit) < 0) {
finalLimit = mem
}
}
}
}
}
return finalLimit, nil
}
View
@@ -0,0 +1,145 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package mutating_webhook
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
k8sv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/cache"
"kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/testutils"
)
var _ = Describe("Mutating Webhook Namespace Limits", func() {
var vmi v1.VirtualMachineInstance
var namespaceLimit *k8sv1.LimitRange
var namespaceLimitInformer cache.SharedIndexInformer
memory, _ := resource.ParseQuantity("64M")
limitMemory, _ := resource.ParseQuantity("128M")
zeroMemory, _ := resource.ParseQuantity("0M")
BeforeEach(func() {
vmi = v1.VirtualMachineInstance{
Spec: v1.VirtualMachineInstanceSpec{
Domain: v1.DomainSpec{
Resources: v1.ResourceRequirements{
Requests: k8sv1.ResourceList{
"memory": memory,
},
},
},
},
}
namespaceLimit = &k8sv1.LimitRange{
Spec: k8sv1.LimitRangeSpec{
Limits: []k8sv1.LimitRangeItem{
{
Type: k8sv1.LimitTypeContainer,
Default: k8sv1.ResourceList{
k8sv1.ResourceMemory: limitMemory,
},
},
},
},
}
namespaceLimitInformer, _ = testutils.NewFakeInformerFor(&k8sv1.LimitRange{})
namespaceLimitInformer.GetIndexer().Add(namespaceLimit)
})
When("VMI has limits under spec", func() {
It("should not apply namespace limits", func() {
vmiCopy := vmi.DeepCopy()
vmiCopy.Spec.Domain.Resources.Limits = k8sv1.ResourceList{
k8sv1.ResourceMemory: memory,
}
By("Applying namespace range values on the VMI")
applyNamespaceLimitRangeValues(vmiCopy, namespaceLimitInformer)
Expect(vmiCopy.Spec.Domain.Resources.Limits.Memory().String()).To(Equal("64M"))
})
})
When("VMI does not have limits under spec", func() {
It("should apply namespace limits", func() {
vmiCopy := vmi.DeepCopy()
By("Applying namespace range values on the VMI")
applyNamespaceLimitRangeValues(vmiCopy, namespaceLimitInformer)
Expect(vmiCopy.Spec.Domain.Resources.Limits.Memory().String()).To(Equal("128M"))
})
When("namespace limit equals 0", func() {
It("should not apply namespace limits", func() {
vmiCopy := vmi.DeepCopy()
namespaceLimitCopy := namespaceLimit.DeepCopy()
namespaceLimitCopy.Spec.Limits[0].Default[k8sv1.ResourceMemory] = zeroMemory
namespaceLimitInformer.GetIndexer().Update(namespaceLimitCopy)
By("Applying namespace range values on the VMI")
applyNamespaceLimitRangeValues(vmiCopy, namespaceLimitInformer)
_, ok := vmiCopy.Spec.Domain.Resources.Limits[k8sv1.ResourceMemory]
Expect(ok).To(Equal(false))
})
AfterEach(func() {
namespaceLimitInformer.GetIndexer().Update(namespaceLimit)
})
})
When("namespace has more than one limit range", func() {
var additionalNamespaceLimit *k8sv1.LimitRange
BeforeEach(func() {
additionalLimitMemory, _ := resource.ParseQuantity("76M")
additionalNamespaceLimit = &k8sv1.LimitRange{
ObjectMeta: k8smetav1.ObjectMeta{
Name: "additional-limit-range",
},
Spec: k8sv1.LimitRangeSpec{
Limits: []k8sv1.LimitRangeItem{
{
Type: k8sv1.LimitTypeContainer,
Default: k8sv1.ResourceList{
k8sv1.ResourceMemory: additionalLimitMemory,
},
},
},
},
}
namespaceLimitInformer.GetIndexer().Add(additionalNamespaceLimit)
})
It("should apply range with minimal limit", func() {
vmiCopy := vmi.DeepCopy()
By("Applying namespace range values on the VMI")
applyNamespaceLimitRangeValues(vmiCopy, namespaceLimitInformer)
Expect(vmiCopy.Spec.Domain.Resources.Limits.Memory().String()).To(Equal("76M"))
})
})
})
})
View
@@ -0,0 +1,263 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2018 Red Hat, Inc.
*
*/
package mutating_webhook
import (
"fmt"
"reflect"
k8sv1 "k8s.io/api/core/v1"
k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/tools/cache"
kubev1 "kubevirt.io/kubevirt/pkg/api/v1"
"kubevirt.io/kubevirt/pkg/log"
)
const exclusionMarking = "virtualmachineinstancepresets.admission.kubevirt.io/exclude"
// listPresets returns all VirtualMachinePresets by namespace
func listPresets(vmiPresetInformer cache.SharedIndexInformer, namespace string) ([]kubev1.VirtualMachineInstancePreset, error) {
indexer := vmiPresetInformer.GetIndexer()
selector := labels.NewSelector()
result := []kubev1.VirtualMachineInstancePreset{}
err := cache.ListAllByNamespace(indexer, namespace, selector, func(obj interface{}) {
preset := obj.(*kubev1.VirtualMachineInstancePreset)
result = append(result, *preset)
})
return result, err
}
// filterPresets returns list of VirtualMachinePresets which match given VirtualMachineInstance.
func filterPresets(list []kubev1.VirtualMachineInstancePreset, vmi *kubev1.VirtualMachineInstance) ([]kubev1.VirtualMachineInstancePreset, error) {
matchingPresets := []kubev1.VirtualMachineInstancePreset{}
for _, preset := range list {
selector, err := k8smetav1.LabelSelectorAsSelector(&preset.Spec.Selector)
if err != nil {
return matchingPresets, err
} else if selector.Matches(labels.Set(vmi.GetLabels())) {
log.Log.Object(vmi).Infof("VirtualMachineInstancePreset %s matches VirtualMachineInstance", preset.GetName())
matchingPresets = append(matchingPresets, preset)
}
}
return matchingPresets, nil
}
func checkMergeConflicts(presetSpec *kubev1.DomainSpec, vmiSpec *kubev1.DomainSpec) error {
errors := []error{}
if len(presetSpec.Resources.Requests) > 0 {
for key, presetReq := range presetSpec.Resources.Requests {
if vmiReq, ok := vmiSpec.Resources.Requests[key]; ok {
if presetReq != vmiReq {
errors = append(errors, fmt.Errorf("spec.resources.requests[%s]: %v != %v", key, presetReq, vmiReq))
}
}
}
}
if presetSpec.CPU != nil && vmiSpec.CPU != nil {
if !reflect.DeepEqual(presetSpec.CPU, vmiSpec.CPU) {
errors = append(errors, fmt.Errorf("spec.cpu: %v != %v", presetSpec.CPU, vmiSpec.CPU))
}
}
if presetSpec.Firmware != nil && vmiSpec.Firmware != nil {
if !reflect.DeepEqual(presetSpec.Firmware, vmiSpec.Firmware) {
errors = append(errors, fmt.Errorf("spec.firmware: %v != %v", presetSpec.Firmware, vmiSpec.Firmware))
}
}
if presetSpec.Clock != nil && vmiSpec.Clock != nil {
if !reflect.DeepEqual(presetSpec.Clock.ClockOffset, vmiSpec.Clock.ClockOffset) {
errors = append(errors, fmt.Errorf("spec.clock.clockoffset: %v != %v", presetSpec.Clock.ClockOffset, vmiSpec.Clock.ClockOffset))
}
if presetSpec.Clock.Timer != nil && vmiSpec.Clock.Timer != nil {
if !reflect.DeepEqual(presetSpec.Clock.Timer, vmiSpec.Clock.Timer) {
errors = append(errors, fmt.Errorf("spec.clock.timer: %v != %v", presetSpec.Clock.Timer, vmiSpec.Clock.Timer))
}
}
}
if presetSpec.Features != nil && vmiSpec.Features != nil {
if !reflect.DeepEqual(presetSpec.Features, vmiSpec.Features) {
errors = append(errors, fmt.Errorf("spec.features: %v != %v", presetSpec.Features, vmiSpec.Features))
}
}
if presetSpec.Devices.Watchdog != nil && vmiSpec.Devices.Watchdog != nil {
if !reflect.DeepEqual(presetSpec.Devices.Watchdog, vmiSpec.Devices.Watchdog) {
errors = append(errors, fmt.Errorf("spec.devices.watchdog: %v != %v", presetSpec.Devices.Watchdog, vmiSpec.Devices.Watchdog))
}
}
if len(errors) > 0 {
return utilerrors.NewAggregate(errors)
}
return nil
}
func mergeDomainSpec(presetSpec *kubev1.DomainSpec, vmiSpec *kubev1.DomainSpec) (bool, error) {
presetConflicts := checkMergeConflicts(presetSpec, vmiSpec)
applied := false
if len(presetSpec.Resources.Requests) > 0 {
if vmiSpec.Resources.Requests == nil {
vmiSpec.Resources.Requests = k8sv1.ResourceList{}
for key, val := range presetSpec.Resources.Requests {
vmiSpec.Resources.Requests[key] = val
}
}
if reflect.DeepEqual(vmiSpec.Resources.Requests, presetSpec.Resources.Requests) {
applied = true
}
}
if presetSpec.CPU != nil {
if vmiSpec.CPU == nil {
vmiSpec.CPU = &kubev1.CPU{}
presetSpec.CPU.DeepCopyInto(vmiSpec.CPU)
}
if reflect.DeepEqual(vmiSpec.CPU, presetSpec.CPU) {
applied = true
}
}
if presetSpec.Firmware != nil {
if vmiSpec.Firmware == nil {
vmiSpec.Firmware = &kubev1.Firmware{}
presetSpec.Firmware.DeepCopyInto(vmiSpec.Firmware)
}
if reflect.DeepEqual(vmiSpec.Firmware, presetSpec.Firmware) {
applied = true
}
}
if presetSpec.Clock != nil {
if vmiSpec.Clock == nil {
vmiSpec.Clock = &kubev1.Clock{}
vmiSpec.Clock.ClockOffset = presetSpec.Clock.ClockOffset
}
if reflect.DeepEqual(vmiSpec.Clock, presetSpec.Clock) {
applied = true
}
if presetSpec.Clock.Timer != nil {
if vmiSpec.Clock.Timer == nil {
vmiSpec.Clock.Timer = &kubev1.Timer{}
presetSpec.Clock.Timer.DeepCopyInto(vmiSpec.Clock.Timer)
}
if reflect.DeepEqual(vmiSpec.Clock.Timer, presetSpec.Clock.Timer) {
applied = true
}
}
}
if presetSpec.Features != nil {
if vmiSpec.Features == nil {
vmiSpec.Features = &kubev1.Features{}
presetSpec.Features.DeepCopyInto(vmiSpec.Features)
}
if reflect.DeepEqual(vmiSpec.Features, presetSpec.Features) {
applied = true
}
}
if presetSpec.Devices.Watchdog != nil {
if vmiSpec.Devices.Watchdog == nil {
vmiSpec.Devices.Watchdog = &kubev1.Watchdog{}
presetSpec.Devices.Watchdog.DeepCopyInto(vmiSpec.Devices.Watchdog)
}
if reflect.DeepEqual(vmiSpec.Devices.Watchdog, presetSpec.Devices.Watchdog) {
applied = true
}
}
return applied, presetConflicts
}
// Compare the domain of every preset to ensure they can all be applied cleanly
func checkPresetConflicts(presets []kubev1.VirtualMachineInstancePreset) error {
errors := []error{}
visitedPresets := []kubev1.VirtualMachineInstancePreset{}
for _, preset := range presets {
for _, visited := range visitedPresets {
err := checkMergeConflicts(preset.Spec.Domain, visited.Spec.Domain)
if err != nil {
errors = append(errors, fmt.Errorf("presets '%s' and '%s' conflict: %v", preset.Name, visited.Name, err))
}
}
visitedPresets = append(visitedPresets, preset)
}
if len(errors) > 0 {
return utilerrors.NewAggregate(errors)
}
return nil
}
func applyPresets(vmi *kubev1.VirtualMachineInstance, presetInformer cache.SharedIndexInformer) error {
if isVMIExcluded(vmi) {
log.Log.Object(vmi).Info("VMI excluded from preset logic")
return nil
}
presets, err := listPresets(presetInformer, vmi.Namespace)
if err != nil {
return err
}
presets, err = filterPresets(presets, vmi)
if err != nil {
return err
}
if len(presets) == 0 {
log.Log.Object(vmi).V(4).Infof("Unable to find any preset that can be accepted to the VMI %s", vmi.Name)
return nil
}
err = checkPresetConflicts(presets)
if err != nil {
return fmt.Errorf("VirtualMachinePresets cannot be applied due to conflicts: %v", err)
}
for _, preset := range presets {
applied, err := mergeDomainSpec(preset.Spec.Domain, &vmi.Spec.Domain)
if applied {
if err != nil {
log.Log.Object(vmi).Warningf("Some settings were not applied for VirtualMachineInstancePreset '%s': %v", preset.Name, err)
}
annotateVMI(vmi, preset)
log.Log.Object(vmi).V(4).Infof("Apply preset %s", preset.Name)
} else {
log.Log.Object(vmi).Warningf("Unable to apply VirtualMachineInstancePreset '%s': %v", preset.Name, err)
}
}
return nil
}
func annotateVMI(vmi *kubev1.VirtualMachineInstance, preset kubev1.VirtualMachineInstancePreset) {
if vmi.Annotations == nil {
vmi.Annotations = map[string]string{}
}
annotationKey := fmt.Sprintf("virtualmachinepreset.%s/%s", kubev1.GroupName, preset.Name)
vmi.Annotations[annotationKey] = kubev1.GroupVersion.String()
}
func isVMIExcluded(vmi *kubev1.VirtualMachineInstance) bool {
if vmi.Annotations != nil {
excluded, ok := vmi.Annotations[exclusionMarking]
return ok && (excluded == "true")
}
return false
}
Oops, something went wrong.