| @@ -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 | ||
| } |
| @@ -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()) | ||
| }) | ||
| }) |
| @@ -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 | ||
| } | ||
| } |
| @@ -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") | ||
| } |
| @@ -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 | ||
| } |
| @@ -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) | ||
| } |
| @@ -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 | ||
| } |
| @@ -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))) | ||
| }) | ||
| }) | ||
| }) |
| @@ -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") | ||
| } |
| @@ -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 | ||
| } |
| @@ -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" |
| @@ -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) | ||
| } |
| @@ -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") | ||
| } |
| @@ -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)) | ||
| }) | ||
| }) | ||
| }) |
| @@ -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 | ||
| } |
| @@ -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")) | ||
| }) | ||
| }) | ||
| }) | ||
| }) |
| @@ -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 | ||
| } |