containerized-data-importer/pkg/apiserver/webhooks/datavolume-validate.go
Michael Henriksen e4fa647eea Clone permission refactoring (#906)
* allow user with 'create pod' permission in source namespace to clone PVCs also refactored Kubevirt interface to clone permission check

* make sure DataVolume spec is immutable also don't check perms when clone source namespace = dest namespace
2019-08-05 11:04:03 -04:00

295 lines
10 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 (
"encoding/json"
"fmt"
"net/url"
"reflect"
"k8s.io/api/admission/v1beta1"
v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sfield "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/client-go/kubernetes"
"k8s.io/klog"
cdicorev1alpha1 "kubevirt.io/containerized-data-importer/pkg/apis/core/v1alpha1"
"kubevirt.io/containerized-data-importer/pkg/controller"
)
type dataVolumeValidatingWebhook struct {
client kubernetes.Interface
}
func validateSourceURL(sourceURL string) string {
if sourceURL == "" {
return "source URL is empty"
}
url, err := url.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
// name of data volume cannot be more than 55 characters (not including '-scratch')
if len(name) > 55 {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("Name of data volume cannot be more than 55 characters"),
Field: "",
})
}
return causes
}
func (wh *dataVolumeValidatingWebhook) validateDataVolumeSpec(request *v1beta1.AdmissionRequest, field *k8sfield.Path, spec *cdicorev1alpha1.DataVolumeSpec) []metav1.StatusCause {
var causes []metav1.StatusCause
var url string
var sourceType string
// spec source field should not be empty
if &spec.Source == nil || (spec.Source.HTTP == nil && spec.Source.S3 == nil && spec.Source.PVC == nil && spec.Source.Upload == nil &&
spec.Source.Blank == nil && spec.Source.Registry == nil) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("Missing Data volume source"),
Field: field.Child("source").String(),
})
return causes
}
if (spec.Source.HTTP != nil && (spec.Source.S3 != nil || spec.Source.PVC != nil || spec.Source.Upload != nil || spec.Source.Blank != nil || spec.Source.Registry != nil)) ||
(spec.Source.S3 != nil && (spec.Source.PVC != nil || spec.Source.Upload != nil || spec.Source.Blank != nil || spec.Source.Registry != nil)) ||
(spec.Source.PVC != nil && (spec.Source.Upload != nil || spec.Source.Blank != nil || spec.Source.Registry != nil)) ||
(spec.Source.Upload != nil && (spec.Source.Blank != nil || spec.Source.Registry != nil)) ||
(spec.Source.Blank != nil && spec.Source.Registry != nil) {
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 or S3, check if URL is valid
if spec.Source.HTTP != nil || spec.Source.S3 != 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()
}
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(cdicorev1alpha1.DataVolumeKubeVirt) && string(spec.ContentType) != string(cdicorev1alpha1.DataVolumeArchive) {
sourceType = field.Child("contentType").String()
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("ContentType not one of: %s, %s", cdicorev1alpha1.DataVolumeKubeVirt, cdicorev1alpha1.DataVolumeArchive),
Field: sourceType,
})
return causes
}
if spec.Source.Blank != nil && string(spec.ContentType) == string(cdicorev1alpha1.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 && spec.ContentType != "" && string(spec.ContentType) != string(cdicorev1alpha1.DataVolumeKubeVirt) {
sourceType = field.Child("contentType").String()
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("ContentType must be " + string(cdicorev1alpha1.DataVolumeKubeVirt) + " when Source is Registry"),
Field: sourceType,
})
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 wh.client != nil && request.Operation == v1beta1.Create {
sourcePVC, err := wh.client.CoreV1().PersistentVolumeClaims(spec.Source.PVC.Namespace).Get(spec.Source.PVC.Name, metav1.GetOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueNotFound,
Message: fmt.Sprintf("Source PVC %s/%s doesn't exist", spec.Source.PVC.Namespace, spec.Source.PVC.Name),
Field: field.Child("source", "PVC").String(),
})
return causes
}
}
err = controller.ValidateCanCloneSourceAndTargetSpec(&sourcePVC.Spec, spec.PVC)
if err != nil {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: err.Error(),
Field: field.Child("PVC").String(),
})
return causes
}
}
}
if spec.PVC == nil {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("Missing Data volume PVC"),
Field: field.Child("PVC").String(),
})
return causes
}
if pvcSize, ok := spec.PVC.Resources.Requests["storage"]; ok {
if pvcSize.IsZero() || pvcSize.Value() < 0 {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("PVC size can't be equal or less than zero"),
Field: field.Child("PVC", "resources", "requests", "size").String(),
})
return causes
}
} else {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("PVC size is missing"),
Field: field.Child("PVC", "resources", "requests", "size").String(),
})
return causes
}
accessModes := spec.PVC.AccessModes
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
}
return causes
}
func (wh *dataVolumeValidatingWebhook) Admit(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
if err := validateDataVolumeResource(ar); err != nil {
return toAdmissionResponseError(err)
}
raw := ar.Request.Object.Raw
dv := cdicorev1alpha1.DataVolume{}
err := json.Unmarshal(raw, &dv)
if err != nil {
return toAdmissionResponseError(err)
}
if ar.Request.Operation == v1beta1.Update {
oldDV := cdicorev1alpha1.DataVolume{}
err = json.Unmarshal(ar.Request.OldObject.Raw, &oldDV)
if err != nil {
return toAdmissionResponseError(err)
}
if !reflect.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 wh.client != nil && ar.Request.Operation == v1beta1.Create {
pvc, err := wh.client.CoreV1().PersistentVolumeClaims(dv.GetNamespace()).Get(dv.GetName(), metav1.GetOptions{})
if err != nil && !k8serrors.IsNotFound(err) {
return toAdmissionResponseError(err)
}
if pvc != nil && pvc.Name != "" {
klog.Errorf("destination PVC %s/%s already exists", dv.GetNamespace(), dv.GetName())
var causes []metav1.StatusCause
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueDuplicate,
Message: fmt.Sprintf("Destination PVC already exists"),
Field: k8sfield.NewPath("DataVolume").Child("Name").String(),
})
return toRejectedAdmissionResponse(causes)
}
}
causes = wh.validateDataVolumeSpec(ar.Request, k8sfield.NewPath("spec"), &dv.Spec)
if len(causes) > 0 {
klog.Infof("rejected DataVolume admission")
return toRejectedAdmissionResponse(causes)
}
reviewResponse := v1beta1.AdmissionResponse{}
reviewResponse.Allowed = true
return &reviewResponse
}