mirror of
https://github.com/kubevirt/containerized-data-importer.git
synced 2025-06-03 06:30:22 +00:00

* Support registry import using node docker cache The new CRI (container runtime interface) importer pod is created with three containers and a shared emptyDir volume: -Init container: copies static http server binary to empty dir -Server container: container image container configured to run the http binary and serve up the image file in /data -Client container: import.sh uses cdi-import to import from server container, and writes "done" file on emptydir -Server container sees "done" file and exits Thanks mhenriks for the PoC! Done: -added ImportMethod to DataVolumeSourceRegistry (DataVolume.Spec.Source.Registry, DataImportCron.Spec.Source.Registry). Import method can be "skopeo" (default), or "cri" for container runtime interface based import -added cdi-containerimage-server & import.sh to the cdi-importer container ToDo: -utests and func tests -doc Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Add tests, fix CR comments Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * CR fixes Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Use deployment docker prefix and tag in func tests Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Add OpenShift ImageStreams import support Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Add importer pod lookup annotation for image streams Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Add pullMethod and imageStream doc Signed-off-by: Arnon Gilboa <agilboa@redhat.com>
554 lines
20 KiB
Go
554 lines
20 KiB
Go
/*
|
|
* This file is part of the CDI 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 2019 Red Hat, Inc.
|
|
*
|
|
*/
|
|
|
|
package webhooks
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
neturl "net/url"
|
|
"reflect"
|
|
|
|
admissionv1 "k8s.io/api/admission/v1"
|
|
v1 "k8s.io/api/core/v1"
|
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
kvalidation "k8s.io/apimachinery/pkg/util/validation"
|
|
k8sfield "k8s.io/apimachinery/pkg/util/validation/field"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/klog/v2"
|
|
|
|
cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/core/v1beta1"
|
|
cdiclient "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned"
|
|
"kubevirt.io/containerized-data-importer/pkg/controller"
|
|
)
|
|
|
|
type dataVolumeValidatingWebhook struct {
|
|
k8sClient kubernetes.Interface
|
|
cdiClient cdiclient.Interface
|
|
}
|
|
|
|
func validateSourceURL(sourceURL string) string {
|
|
if sourceURL == "" {
|
|
return "source URL is empty"
|
|
}
|
|
url, err := neturl.ParseRequestURI(sourceURL)
|
|
if err != nil {
|
|
return fmt.Sprintf("Invalid source URL: %s", sourceURL)
|
|
}
|
|
if url.Scheme != "http" && url.Scheme != "https" {
|
|
return fmt.Sprintf("Invalid source URL scheme: %s", sourceURL)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func validateDataVolumeName(name string) []metav1.StatusCause {
|
|
var causes []metav1.StatusCause
|
|
if len(name) > kvalidation.DNS1123SubdomainMaxLength {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Name of data volume cannot be more than %d characters", kvalidation.DNS1123SubdomainMaxLength),
|
|
Field: "",
|
|
})
|
|
}
|
|
return causes
|
|
}
|
|
|
|
func validateContentTypes(sourcePVC *v1.PersistentVolumeClaim, spec *cdiv1.DataVolumeSpec) (bool, cdiv1.DataVolumeContentType, cdiv1.DataVolumeContentType) {
|
|
sourceContentType := cdiv1.DataVolumeContentType(controller.GetContentType(sourcePVC))
|
|
targetContentType := spec.ContentType
|
|
if targetContentType == "" {
|
|
targetContentType = cdiv1.DataVolumeKubeVirt
|
|
}
|
|
return sourceContentType == targetContentType, sourceContentType, targetContentType
|
|
}
|
|
|
|
func (wh *dataVolumeValidatingWebhook) validateDataVolumeSpec(request *admissionv1.AdmissionRequest, field *k8sfield.Path, spec *cdiv1.DataVolumeSpec, namespace *string) []metav1.StatusCause {
|
|
var causes []metav1.StatusCause
|
|
var url string
|
|
var sourceType string
|
|
|
|
if spec.PVC == nil && spec.Storage == nil {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Missing Data volume PVC"),
|
|
Field: field.Child("PVC").String(),
|
|
})
|
|
return causes
|
|
}
|
|
if spec.PVC != nil && spec.Storage != nil {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Duplicate storage definition, both target storage and target pvc defined"),
|
|
Field: field.Child("PVC", "Storage").String(),
|
|
})
|
|
return causes
|
|
}
|
|
if spec.PVC != nil {
|
|
cause, valid := validateStorageSize(spec.PVC.Resources, field, "PVC")
|
|
if !valid {
|
|
causes = append(causes, *cause)
|
|
return causes
|
|
}
|
|
accessModes := spec.PVC.AccessModes
|
|
if len(accessModes) == 0 {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Required value: at least 1 access mode is required"),
|
|
Field: field.Child("PVC", "accessModes").String(),
|
|
})
|
|
return causes
|
|
}
|
|
if len(accessModes) > 1 {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("PVC multiple accessModes"),
|
|
Field: field.Child("PVC", "accessModes").String(),
|
|
})
|
|
return causes
|
|
}
|
|
// We know we have one access mode
|
|
if accessModes[0] != v1.ReadWriteOnce && accessModes[0] != v1.ReadOnlyMany && accessModes[0] != v1.ReadWriteMany {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Unsupported value: \"%s\": supported values: \"ReadOnlyMany\", \"ReadWriteMany\", \"ReadWriteOnce\"", string(accessModes[0])),
|
|
Field: field.Child("PVC", "accessModes").String(),
|
|
})
|
|
return causes
|
|
}
|
|
} else if spec.Storage != nil {
|
|
cause, valid := validateStorageSize(spec.Storage.Resources, field, "Storage")
|
|
if !valid {
|
|
causes = append(causes, *cause)
|
|
return causes
|
|
}
|
|
// here in storage spec we allow empty access mode and AccessModes with more than one entry
|
|
accessModes := spec.Storage.AccessModes
|
|
for _, mode := range accessModes {
|
|
if mode != v1.ReadWriteOnce && mode != v1.ReadOnlyMany && mode != v1.ReadWriteMany {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Unsupported value: \"%s\": supported values: \"ReadOnlyMany\", \"ReadWriteMany\", \"ReadWriteOnce\"", string(accessModes[0])),
|
|
Field: field.Child("PVC", "accessModes").String(),
|
|
})
|
|
return causes
|
|
}
|
|
}
|
|
}
|
|
|
|
if (spec.Source == nil && spec.SourceRef == nil) || (spec.Source != nil && spec.SourceRef != nil) {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Data volume should have either Source or SourceRef"),
|
|
Field: field.Child("source").String(),
|
|
})
|
|
return causes
|
|
}
|
|
if spec.SourceRef != nil {
|
|
cause := wh.validateSourceRef(request, spec, field, namespace)
|
|
if cause != nil {
|
|
causes = append(causes, *cause)
|
|
}
|
|
return causes
|
|
}
|
|
|
|
numberOfSources := 0
|
|
s := reflect.ValueOf(spec.Source).Elem()
|
|
for i := 0; i < s.NumField(); i++ {
|
|
if !reflect.ValueOf(s.Field(i).Interface()).IsNil() {
|
|
numberOfSources++
|
|
}
|
|
}
|
|
if numberOfSources == 0 {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Missing Data volume source"),
|
|
Field: field.Child("source").String(),
|
|
})
|
|
return causes
|
|
}
|
|
if numberOfSources > 1 {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Multiple Data volume sources"),
|
|
Field: field.Child("source").String(),
|
|
})
|
|
return causes
|
|
}
|
|
// if source types are HTTP, Imageio, S3 or VDDK, check if URL is valid
|
|
if spec.Source.HTTP != nil || spec.Source.S3 != nil || spec.Source.Imageio != nil || spec.Source.VDDK != nil {
|
|
if spec.Source.HTTP != nil {
|
|
url = spec.Source.HTTP.URL
|
|
sourceType = field.Child("source", "HTTP", "url").String()
|
|
} else if spec.Source.S3 != nil {
|
|
url = spec.Source.S3.URL
|
|
sourceType = field.Child("source", "S3", "url").String()
|
|
} else if spec.Source.Imageio != nil {
|
|
url = spec.Source.Imageio.URL
|
|
sourceType = field.Child("source", "Imageio", "url").String()
|
|
} else if spec.Source.VDDK != nil {
|
|
url = spec.Source.VDDK.URL
|
|
sourceType = field.Child("source", "VDDK", "url").String()
|
|
}
|
|
err := validateSourceURL(url)
|
|
if err != "" {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("%s %s", field.Child("source").String(), err),
|
|
Field: sourceType,
|
|
})
|
|
return causes
|
|
}
|
|
}
|
|
|
|
// Make sure contentType is either empty (kubevirt), or kubevirt or archive
|
|
if spec.ContentType != "" && string(spec.ContentType) != string(cdiv1.DataVolumeKubeVirt) && string(spec.ContentType) != string(cdiv1.DataVolumeArchive) {
|
|
sourceType = field.Child("contentType").String()
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("ContentType not one of: %s, %s", cdiv1.DataVolumeKubeVirt, cdiv1.DataVolumeArchive),
|
|
Field: sourceType,
|
|
})
|
|
return causes
|
|
}
|
|
|
|
if spec.Source.Blank != nil && string(spec.ContentType) == string(cdiv1.DataVolumeArchive) {
|
|
sourceType = field.Child("contentType").String()
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("SourceType cannot be blank and the contentType be archive"),
|
|
Field: sourceType,
|
|
})
|
|
return causes
|
|
}
|
|
|
|
if spec.Source.Registry != nil {
|
|
if spec.ContentType != "" && string(spec.ContentType) != string(cdiv1.DataVolumeKubeVirt) {
|
|
sourceType = field.Child("contentType").String()
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("ContentType must be %s when Source is Registry", cdiv1.DataVolumeKubeVirt),
|
|
Field: sourceType,
|
|
})
|
|
return causes
|
|
}
|
|
sourceURL := spec.Source.Registry.URL
|
|
sourceIS := spec.Source.Registry.ImageStream
|
|
if (sourceURL == nil && sourceIS == nil) || (sourceURL != nil && sourceIS != nil) {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Source registry should have either URL or ImageStream"),
|
|
Field: field.Child("source", "Registry").String(),
|
|
})
|
|
return causes
|
|
}
|
|
if sourceURL != nil {
|
|
url, err := neturl.Parse(*sourceURL)
|
|
if err != nil {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Illegal registry source URL %s", *sourceURL),
|
|
Field: field.Child("source", "Registry", "URL").String(),
|
|
})
|
|
return causes
|
|
}
|
|
scheme := url.Scheme
|
|
if scheme != cdiv1.RegistrySchemeDocker && scheme != cdiv1.RegistrySchemeOci {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Illegal registry source URL scheme %s", url),
|
|
Field: field.Child("source", "Registry", "URL").String(),
|
|
})
|
|
return causes
|
|
}
|
|
}
|
|
importMethod := spec.Source.Registry.PullMethod
|
|
if importMethod != nil && *importMethod != cdiv1.RegistryPullPod && *importMethod != cdiv1.RegistryPullNode {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("ImportMethod %s is neither %s, %s or \"\"", *importMethod, cdiv1.RegistryPullPod, cdiv1.RegistryPullNode),
|
|
Field: field.Child("source", "Registry", "importMethod").String(),
|
|
})
|
|
return causes
|
|
}
|
|
|
|
if sourceIS != nil && *sourceIS == "" {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Source registry ImageStream is not valid"),
|
|
Field: field.Child("source", "Registry", "importMethod").String(),
|
|
})
|
|
return causes
|
|
}
|
|
|
|
if sourceIS != nil && (importMethod == nil || *importMethod != cdiv1.RegistryPullNode) {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Source registry ImageStream is supported only with node pull import method"),
|
|
Field: field.Child("source", "Registry", "importMethod").String(),
|
|
})
|
|
return causes
|
|
}
|
|
}
|
|
|
|
if spec.Source.Imageio != nil {
|
|
if spec.Source.Imageio.SecretRef == "" || spec.Source.Imageio.CertConfigMap == "" || spec.Source.Imageio.DiskID == "" {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("%s source Imageio is not valid", field.Child("source", "Imageio").String()),
|
|
Field: field.Child("source", "Imageio").String(),
|
|
})
|
|
return causes
|
|
}
|
|
}
|
|
|
|
if spec.Source.VDDK != nil {
|
|
if spec.Source.VDDK.SecretRef == "" || spec.Source.VDDK.UUID == "" || spec.Source.VDDK.BackingFile == "" || spec.Source.VDDK.Thumbprint == "" {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("%s source VDDK is not valid", field.Child("source", "VDDK").String()),
|
|
Field: field.Child("source", "VDDK").String(),
|
|
})
|
|
return causes
|
|
}
|
|
}
|
|
|
|
if spec.Source.PVC != nil {
|
|
if spec.Source.PVC.Namespace == "" || spec.Source.PVC.Name == "" {
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("%s source PVC is not valid", field.Child("source", "PVC").String()),
|
|
Field: field.Child("source", "PVC").String(),
|
|
})
|
|
return causes
|
|
}
|
|
if request.Operation == admissionv1.Create {
|
|
cause := wh.validateDataVolumeSourcePVC(spec.Source.PVC, field.Child("source", "PVC"), spec)
|
|
if cause != nil {
|
|
causes = append(causes, *cause)
|
|
}
|
|
}
|
|
}
|
|
|
|
return causes
|
|
}
|
|
|
|
func (wh *dataVolumeValidatingWebhook) validateSourceRef(request *admissionv1.AdmissionRequest, spec *cdiv1.DataVolumeSpec, field *k8sfield.Path, namespace *string) *metav1.StatusCause {
|
|
if spec.SourceRef.Kind == "" {
|
|
return &metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Missing sourceRef kind"),
|
|
Field: field.Child("sourceRef", "Kind").String(),
|
|
}
|
|
}
|
|
if spec.SourceRef.Kind != cdiv1.DataVolumeDataSource {
|
|
return &metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Unsupported sourceRef kind %s, currently only %s is supported", spec.SourceRef.Kind, cdiv1.DataVolumeDataSource),
|
|
Field: field.Child("sourceRef", "Kind").String(),
|
|
}
|
|
}
|
|
if spec.SourceRef.Name == "" {
|
|
return &metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Missing sourceRef name"),
|
|
Field: field.Child("sourceRef", "Name").String(),
|
|
}
|
|
}
|
|
if request.Operation != admissionv1.Create {
|
|
return nil
|
|
}
|
|
ns := namespace
|
|
if spec.SourceRef.Namespace != nil && *spec.SourceRef.Namespace != "" {
|
|
ns = spec.SourceRef.Namespace
|
|
}
|
|
dataSource, err := wh.cdiClient.CdiV1beta1().DataSources(*ns).Get(context.TODO(), spec.SourceRef.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
return &metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueNotFound,
|
|
Message: fmt.Sprintf("SourceRef %s/%s/%s not found", spec.SourceRef.Kind, *ns, spec.SourceRef.Name),
|
|
Field: field.Child("sourceRef").String(),
|
|
}
|
|
}
|
|
return &metav1.StatusCause{
|
|
Message: err.Error(),
|
|
Field: field.Child("sourceRef").String(),
|
|
}
|
|
}
|
|
return wh.validateDataVolumeSourcePVC(dataSource.Spec.Source.PVC, field.Child("sourceRef"), spec)
|
|
}
|
|
|
|
func (wh *dataVolumeValidatingWebhook) validateDataVolumeSourcePVC(PVC *cdiv1.DataVolumeSourcePVC, field *k8sfield.Path, spec *cdiv1.DataVolumeSpec) *metav1.StatusCause {
|
|
sourcePVC, err := wh.k8sClient.CoreV1().PersistentVolumeClaims(PVC.Namespace).Get(context.TODO(), PVC.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
return &metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueNotFound,
|
|
Message: fmt.Sprintf("Source PVC %s/%s not found", PVC.Namespace, PVC.Name),
|
|
Field: field.String(),
|
|
}
|
|
}
|
|
return &metav1.StatusCause{
|
|
Message: err.Error(),
|
|
Field: field.String(),
|
|
}
|
|
}
|
|
valid, sourceContentType, targetContentType := validateContentTypes(sourcePVC, spec)
|
|
if !valid {
|
|
return &metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("Source contentType (%s) and target contentType (%s) do not match", sourceContentType, targetContentType),
|
|
Field: field.String(),
|
|
}
|
|
}
|
|
var targetResources v1.ResourceRequirements
|
|
if spec.PVC != nil {
|
|
targetResources = spec.PVC.Resources
|
|
} else {
|
|
targetResources = spec.Storage.Resources
|
|
}
|
|
if err = controller.ValidateCloneSize(sourcePVC.Spec.Resources, targetResources); err != nil {
|
|
return &metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: err.Error(),
|
|
Field: field.String(),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateStorageSize(resources v1.ResourceRequirements, field *k8sfield.Path, name string) (*metav1.StatusCause, bool) {
|
|
if pvcSize, ok := resources.Requests["storage"]; ok {
|
|
if pvcSize.IsZero() || pvcSize.Value() < 0 {
|
|
cause := metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("%s size can't be equal or less than zero", name),
|
|
Field: field.Child(name, "resources", "requests", "size").String(),
|
|
}
|
|
return &cause, false
|
|
}
|
|
} else {
|
|
cause := metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Message: fmt.Sprintf("%s size is missing", name),
|
|
Field: field.Child(name, "resources", "requests", "size").String(),
|
|
}
|
|
return &cause, false
|
|
}
|
|
|
|
return nil, true
|
|
}
|
|
|
|
func (wh *dataVolumeValidatingWebhook) Admit(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
|
|
if err := validateDataVolumeResource(ar); err != nil {
|
|
return toAdmissionResponseError(err)
|
|
}
|
|
|
|
raw := ar.Request.Object.Raw
|
|
dv := cdiv1.DataVolume{}
|
|
|
|
err := json.Unmarshal(raw, &dv)
|
|
if err != nil {
|
|
return toAdmissionResponseError(err)
|
|
}
|
|
|
|
if ar.Request.Operation == admissionv1.Update {
|
|
oldDV := cdiv1.DataVolume{}
|
|
err = json.Unmarshal(ar.Request.OldObject.Raw, &oldDV)
|
|
if err != nil {
|
|
return toAdmissionResponseError(err)
|
|
}
|
|
|
|
// Always admit checkpoint updates for multi-stage migrations.
|
|
multiStageAdmitted := false
|
|
isMultiStage := dv.Spec.Source != nil && len(dv.Spec.Checkpoints) > 0 &&
|
|
(dv.Spec.Source.VDDK != nil || dv.Spec.Source.Imageio != nil)
|
|
if isMultiStage {
|
|
oldSpec := oldDV.Spec.DeepCopy()
|
|
oldSpec.FinalCheckpoint = false
|
|
oldSpec.Checkpoints = nil
|
|
|
|
newSpec := dv.Spec.DeepCopy()
|
|
newSpec.FinalCheckpoint = false
|
|
newSpec.Checkpoints = nil
|
|
|
|
multiStageAdmitted = apiequality.Semantic.DeepEqual(newSpec, oldSpec)
|
|
}
|
|
|
|
if !multiStageAdmitted && !apiequality.Semantic.DeepEqual(dv.Spec, oldDV.Spec) {
|
|
klog.Errorf("Cannot update spec for DataVolume %s/%s", dv.GetNamespace(), dv.GetName())
|
|
var causes []metav1.StatusCause
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueDuplicate,
|
|
Message: fmt.Sprintf("Cannot update DataVolume Spec"),
|
|
Field: k8sfield.NewPath("DataVolume").Child("Spec").String(),
|
|
})
|
|
return toRejectedAdmissionResponse(causes)
|
|
}
|
|
}
|
|
|
|
causes := validateDataVolumeName(dv.Name)
|
|
if len(causes) > 0 {
|
|
klog.Infof("rejected DataVolume admission")
|
|
return toRejectedAdmissionResponse(causes)
|
|
}
|
|
|
|
if ar.Request.Operation == admissionv1.Create {
|
|
pvc, err := wh.k8sClient.CoreV1().PersistentVolumeClaims(dv.GetNamespace()).Get(context.TODO(), dv.GetName(), metav1.GetOptions{})
|
|
if err != nil {
|
|
if !k8serrors.IsNotFound(err) {
|
|
return toAdmissionResponseError(err)
|
|
}
|
|
} else {
|
|
dvName, ok := pvc.Annotations[controller.AnnPopulatedFor]
|
|
if !ok || dvName != dv.GetName() {
|
|
pvcOwner := metav1.GetControllerOf(pvc)
|
|
// We should reject the DV if a PVC with the same name exists, and that PVC has no ownerRef, or that
|
|
// PVC has an ownerRef that is not a DataVolume. Because that means that PVC is not managed by the
|
|
// datavolume controller, and we can't use it.
|
|
if (pvcOwner == nil) || (pvcOwner.Kind != "DataVolume") {
|
|
klog.Errorf("destination PVC %s/%s already exists", pvc.GetNamespace(), pvc.GetName())
|
|
var causes []metav1.StatusCause
|
|
causes = append(causes, metav1.StatusCause{
|
|
Type: metav1.CauseTypeFieldValueDuplicate,
|
|
Message: fmt.Sprintf("Destination PVC %s/%s already exists", pvc.GetNamespace(), pvc.GetName()),
|
|
Field: k8sfield.NewPath("DataVolume").Child("Name").String(),
|
|
})
|
|
return toRejectedAdmissionResponse(causes)
|
|
}
|
|
}
|
|
|
|
klog.Infof("Using initialized PVC %s for DataVolume %s", pvc.GetName(), dv.GetName())
|
|
}
|
|
}
|
|
|
|
causes = wh.validateDataVolumeSpec(ar.Request, k8sfield.NewPath("spec"), &dv.Spec, &dv.Namespace)
|
|
if len(causes) > 0 {
|
|
klog.Infof("rejected DataVolume admission %s", causes)
|
|
return toRejectedAdmissionResponse(causes)
|
|
}
|
|
|
|
reviewResponse := admissionv1.AdmissionResponse{}
|
|
reviewResponse.Allowed = true
|
|
return &reviewResponse
|
|
}
|