add groupsnapshot related webhooks
This commit is contained in:
@@ -20,3 +20,26 @@ webhooks:
|
|||||||
sideEffects: None
|
sideEffects: None
|
||||||
failurePolicy: Ignore # We recommend switching to Fail only after successful installation of the webhook server and webhook.
|
failurePolicy: Ignore # We recommend switching to Fail only after successful installation of the webhook server and webhook.
|
||||||
timeoutSeconds: 2 # This will affect the latency and performance. Finetune this value based on your application's tolerance.
|
timeoutSeconds: 2 # This will affect the latency and performance. Finetune this value based on your application's tolerance.
|
||||||
|
---
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1
|
||||||
|
kind: ValidatingWebhookConfiguration
|
||||||
|
metadata:
|
||||||
|
name: "validation-webhook.groupsnapshot.storage.k8s.io"
|
||||||
|
webhooks:
|
||||||
|
- name: "validation-webhook.groupsnapshot.storage.k8s.io"
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["groupsnapshot.storage.k8s.io"]
|
||||||
|
apiVersions: ["v1alpha1"]
|
||||||
|
operations: ["CREATE", "UPDATE"]
|
||||||
|
resources: ["volumegroupsnapshots", "volumegroupsnapshotcontents", "volumegroupsnapshotclasses"]
|
||||||
|
scope: "*"
|
||||||
|
clientConfig:
|
||||||
|
service:
|
||||||
|
namespace: "default"
|
||||||
|
name: "snapshot-validation-service"
|
||||||
|
path: "/volumegroupsnapshot"
|
||||||
|
caBundle: ${CA_BUNDLE}
|
||||||
|
admissionReviewVersions: ["v1"]
|
||||||
|
sideEffects: None
|
||||||
|
failurePolicy: Ignore # We recommend switching to Fail only after successful installation of the webhook server and webhook.
|
||||||
|
timeoutSeconds: 2 # This will affect the latency and performance. Finetune this value based on your application's tolerance.
|
||||||
|
@@ -19,6 +19,9 @@ rules:
|
|||||||
- apiGroups: ["snapshot.storage.k8s.io"]
|
- apiGroups: ["snapshot.storage.k8s.io"]
|
||||||
resources: ["volumesnapshotclasses"]
|
resources: ["volumesnapshotclasses"]
|
||||||
verbs: ["get", "list", "watch"]
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["groupsnapshot.storage.k8s.io"]
|
||||||
|
resources: ["volumegroupsnapshotclasses"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
---
|
---
|
||||||
kind: ClusterRoleBinding
|
kind: ClusterRoleBinding
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
@@ -77,7 +77,8 @@ const (
|
|||||||
// Name of finalizer on PVCs that is being used as a source to create VolumeSnapshots
|
// Name of finalizer on PVCs that is being used as a source to create VolumeSnapshots
|
||||||
PVCFinalizer = "snapshot.storage.kubernetes.io/pvc-as-source-protection"
|
PVCFinalizer = "snapshot.storage.kubernetes.io/pvc-as-source-protection"
|
||||||
|
|
||||||
IsDefaultSnapshotClassAnnotation = "snapshot.storage.kubernetes.io/is-default-class"
|
IsDefaultSnapshotClassAnnotation = "snapshot.storage.kubernetes.io/is-default-class"
|
||||||
|
IsDefaultGroupSnapshotClassAnnotation = "groupsnapshot.storage.kubernetes.io/is-default-class"
|
||||||
|
|
||||||
// AnnVolumeSnapshotBeingDeleted annotation applies to VolumeSnapshotContents.
|
// AnnVolumeSnapshotBeingDeleted annotation applies to VolumeSnapshotContents.
|
||||||
// It indicates that the common snapshot controller has verified that volume
|
// It indicates that the common snapshot controller has verified that volume
|
||||||
|
254
pkg/validation-webhook/groupsnapshot.go
Normal file
254
pkg/validation-webhook/groupsnapshot.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
volumegroupsnapshotv1alpha1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumegroupsnapshot/v1alpha1"
|
||||||
|
groupsnapshotlisters "github.com/kubernetes-csi/external-snapshotter/client/v6/listers/volumegroupsnapshot/v1alpha1"
|
||||||
|
"github.com/kubernetes-csi/external-snapshotter/v6/pkg/utils"
|
||||||
|
v1 "k8s.io/api/admission/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// GroupSnapshotV1Alpha1GVR is GroupVersionResource for v1alpha1 VolumeGroupSnapshots
|
||||||
|
GroupSnapshotV1Alpha1GVR = metav1.GroupVersionResource{Group: volumegroupsnapshotv1alpha1.GroupName, Version: "v1alpha1", Resource: "volumegroupsnapshots"}
|
||||||
|
// GroupSnapshotContentV1Apha1GVR is GroupVersionResource for v1alpha1 VolumeGroupSnapshotContents
|
||||||
|
GroupSnapshotContentV1Apha1GVR = metav1.GroupVersionResource{Group: volumegroupsnapshotv1alpha1.GroupName, Version: "v1alpha1", Resource: "volumegroupsnapshotcontents"}
|
||||||
|
// GroupSnapshotClassV1Apha1GVR is GroupVersionResource for v1alpha1 VolumeGroupSnapshotClasses
|
||||||
|
GroupSnapshotClassV1Apha1GVR = metav1.GroupVersionResource{Group: volumegroupsnapshotv1alpha1.GroupName, Version: "v1alpha1", Resource: "volumegroupsnapshotclasses"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupSnapshotAdmitter interface {
|
||||||
|
Admit(v1.AdmissionReview) *v1.AdmissionResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupSnapshotAdmitter struct {
|
||||||
|
lister groupsnapshotlisters.VolumeGroupSnapshotClassLister
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGroupSnapshotAdmitter(lister groupsnapshotlisters.VolumeGroupSnapshotClassLister) GroupSnapshotAdmitter {
|
||||||
|
return &groupSnapshotAdmitter{
|
||||||
|
lister: lister,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a label {"added-label": "yes"} to the object
|
||||||
|
func (a groupSnapshotAdmitter) Admit(ar v1.AdmissionReview) *v1.AdmissionResponse {
|
||||||
|
klog.V(2).Info("admitting volumegroupsnapshots volumegroupsnapshotcontents " +
|
||||||
|
"or volumegroupsnapshotclasses")
|
||||||
|
|
||||||
|
reviewResponse := &v1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
Result: &metav1.Status{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admit requests other than Update and Create
|
||||||
|
if !(ar.Request.Operation == v1.Update || ar.Request.Operation == v1.Create) {
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
isUpdate := ar.Request.Operation == v1.Update
|
||||||
|
|
||||||
|
raw := ar.Request.Object.Raw
|
||||||
|
oldRaw := ar.Request.OldObject.Raw
|
||||||
|
|
||||||
|
deserializer := codecs.UniversalDeserializer()
|
||||||
|
switch ar.Request.Resource {
|
||||||
|
case GroupSnapshotV1Alpha1GVR:
|
||||||
|
groupSnapshot := &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{}
|
||||||
|
if _, _, err := deserializer.Decode(raw, nil, groupSnapshot); err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
oldGroupSnapshot := &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{}
|
||||||
|
if _, _, err := deserializer.Decode(oldRaw, nil, oldGroupSnapshot); err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
return decideGroupSnapshotV1Alpha1(groupSnapshot, oldGroupSnapshot, isUpdate)
|
||||||
|
case GroupSnapshotContentV1Apha1GVR:
|
||||||
|
groupSnapContent := &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent{}
|
||||||
|
if _, _, err := deserializer.Decode(raw, nil, groupSnapContent); err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
oldGroupSnapContent := &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent{}
|
||||||
|
if _, _, err := deserializer.Decode(oldRaw, nil, oldGroupSnapContent); err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
return decideGroupSnapshotContentV1Alpha1(groupSnapContent, oldGroupSnapContent, isUpdate)
|
||||||
|
case GroupSnapshotClassV1Apha1GVR:
|
||||||
|
groupSnapClass := &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{}
|
||||||
|
if _, _, err := deserializer.Decode(raw, nil, groupSnapClass); err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
oldGroupSnapClass := &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{}
|
||||||
|
if _, _, err := deserializer.Decode(oldRaw, nil, oldGroupSnapClass); err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
return decideGroupSnapshotClassV1Alpha1(groupSnapClass, oldGroupSnapClass, a.lister)
|
||||||
|
default:
|
||||||
|
err := fmt.Errorf("expect resource to be %s, %s, or %s, but found %v",
|
||||||
|
GroupSnapshotV1Alpha1GVR, GroupSnapshotContentV1Apha1GVR,
|
||||||
|
GroupSnapshotClassV1Apha1GVR, ar.Request.Resource)
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decideGroupSnapshotV1Alpha1(groupSnapshot, oldGroupSnapshot *volumegroupsnapshotv1alpha1.VolumeGroupSnapshot, isUpdate bool) *v1.AdmissionResponse {
|
||||||
|
reviewResponse := &v1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
Result: &metav1.Status{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if isUpdate {
|
||||||
|
// if it is an UPDATE and oldGroupSnapshot is valid, check immutable fields
|
||||||
|
if err := checkGroupSnapshotImmutableFieldsV1Alpha1(groupSnapshot, oldGroupSnapshot); err != nil {
|
||||||
|
reviewResponse.Allowed = false
|
||||||
|
reviewResponse.Result.Message = err.Error()
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Enforce strict validation for CREATE requests. Immutable checks don't apply for CREATE requests.
|
||||||
|
// Enforce strict validation for UPDATE requests where old is valid and passes immutability check.
|
||||||
|
if err := ValidateV1Alpha1GroupSnapshot(groupSnapshot); err != nil {
|
||||||
|
reviewResponse.Allowed = false
|
||||||
|
reviewResponse.Result.Message = err.Error()
|
||||||
|
}
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func decideGroupSnapshotContentV1Alpha1(groupSnapcontent, oldGroupSnapcontent *volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent, isUpdate bool) *v1.AdmissionResponse {
|
||||||
|
reviewResponse := &v1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
Result: &metav1.Status{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if isUpdate {
|
||||||
|
// if it is an UPDATE and oldGroupSnapcontent is valid, check immutable fields
|
||||||
|
if err := checkGroupSnapshotContentImmutableFieldsV1Alpha1(groupSnapcontent, oldGroupSnapcontent); err != nil {
|
||||||
|
reviewResponse.Allowed = false
|
||||||
|
reviewResponse.Result.Message = err.Error()
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Enforce strict validation for all CREATE requests. Immutable checks don't apply for CREATE requests.
|
||||||
|
// Enforce strict validation for UPDATE requests where old is valid and passes immutability check.
|
||||||
|
if err := ValidateV1Alpha1GroupSnapshotContent(groupSnapcontent); err != nil {
|
||||||
|
reviewResponse.Allowed = false
|
||||||
|
reviewResponse.Result.Message = err.Error()
|
||||||
|
}
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func decideGroupSnapshotClassV1Alpha1(groupSnapClass, oldGroupSnapClass *volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass, lister groupsnapshotlisters.VolumeGroupSnapshotClassLister) *v1.AdmissionResponse {
|
||||||
|
reviewResponse := &v1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
Result: &metav1.Status{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only Validate when a new group snapshot class is being set as a default.
|
||||||
|
if groupSnapClass.Annotations[utils.IsDefaultGroupSnapshotClassAnnotation] != "true" {
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the old group snapshot class has this, then we can assume that it was validated if driver is the same.
|
||||||
|
if oldGroupSnapClass.Annotations[utils.IsDefaultGroupSnapshotClassAnnotation] == "true" && oldGroupSnapClass.Driver == groupSnapClass.Driver {
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := lister.List(labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
reviewResponse.Allowed = false
|
||||||
|
reviewResponse.Result.Message = err.Error()
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, groupSnapshotClass := range ret {
|
||||||
|
if groupSnapshotClass.Annotations[utils.IsDefaultGroupSnapshotClassAnnotation] != "true" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if groupSnapshotClass.Driver == groupSnapClass.Driver {
|
||||||
|
reviewResponse.Allowed = false
|
||||||
|
reviewResponse.Result.Message = fmt.Sprintf("default group snapshot class: %v already exists for driver: %v", groupSnapshotClass.Name, groupSnapClass.Driver)
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkGroupSnapshotImmutableFieldsV1Alpha1(groupSnapshot, oldGroupSnapshot *volumegroupsnapshotv1alpha1.VolumeGroupSnapshot) error {
|
||||||
|
if groupSnapshot == nil {
|
||||||
|
return fmt.Errorf("VolumeGroupSnapshot is nil")
|
||||||
|
}
|
||||||
|
if oldGroupSnapshot == nil {
|
||||||
|
return fmt.Errorf("old VolumeGroupSnapshot is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
source := groupSnapshot.Spec.Source
|
||||||
|
oldSource := oldGroupSnapshot.Spec.Source
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(source.Selector, oldSource.Selector) {
|
||||||
|
return fmt.Errorf("Spec.Source.Selector is immutable but was changed from %s to %s", oldSource.Selector, source.Selector)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(source.VolumeGroupSnapshotContentName, oldSource.VolumeGroupSnapshotContentName) {
|
||||||
|
return fmt.Errorf("Spec.Source.VolumeGroupSnapshotContentName is immutable but was changed from %s to %s", strPtrDereference(oldSource.VolumeGroupSnapshotContentName), strPtrDereference(source.VolumeGroupSnapshotContentName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkGroupSnapshotContentImmutableFieldsV1Alpha1(groupSnapcontent, oldGroupSnapcontent *volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent) error {
|
||||||
|
if groupSnapcontent == nil {
|
||||||
|
return fmt.Errorf("VolumeGroupSnapshotContent is nil")
|
||||||
|
}
|
||||||
|
if oldGroupSnapcontent == nil {
|
||||||
|
return fmt.Errorf("old VolumeGroupSnapshotContent is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
source := groupSnapcontent.Spec.Source
|
||||||
|
oldSource := oldGroupSnapcontent.Spec.Source
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(source.VolumeGroupSnapshotHandle, oldSource.VolumeGroupSnapshotHandle) {
|
||||||
|
return fmt.Errorf("Spec.Source.VolumeGroupSnapshotHandle is immutable but was changed from %s to %s", strPtrDereference(oldSource.VolumeGroupSnapshotHandle), strPtrDereference(source.VolumeGroupSnapshotHandle))
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(source.PersistentVolumeNames, oldSource.PersistentVolumeNames) {
|
||||||
|
return fmt.Errorf("Spec.Source.PersistentVolumeNames is immutable but was changed from %v to %v", oldSource.PersistentVolumeNames, source.PersistentVolumeNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := groupSnapcontent.Spec.VolumeGroupSnapshotRef
|
||||||
|
oldRef := oldGroupSnapcontent.Spec.VolumeGroupSnapshotRef
|
||||||
|
|
||||||
|
if ref.Name != oldRef.Name {
|
||||||
|
return fmt.Errorf("Spec.VolumeGroupSnapshotRef.Name is immutable but was changed from %s to %s", oldRef.Name, ref.Name)
|
||||||
|
}
|
||||||
|
if ref.Namespace != oldRef.Namespace {
|
||||||
|
return fmt.Errorf("Spec.VolumeGroupSnapshotRef.Namespace is immutable but was changed from %s to %s", oldRef.Namespace, ref.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
678
pkg/validation-webhook/groupsnapshot_test.go
Normal file
678
pkg/validation-webhook/groupsnapshot_test.go
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
volumegroupsnapshotv1alpha1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumegroupsnapshot/v1alpha1"
|
||||||
|
groupsnapshotlisters "github.com/kubernetes-csi/external-snapshotter/client/v6/listers/volumegroupsnapshot/v1alpha1"
|
||||||
|
"github.com/kubernetes-csi/external-snapshotter/v6/pkg/utils"
|
||||||
|
v1 "k8s.io/api/admission/v1"
|
||||||
|
core_v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeGroupSnapshotLister struct {
|
||||||
|
values []*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeGroupSnapshotLister) List(selector labels.Selector) (ret []*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass, err error) {
|
||||||
|
return f.values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeGroupSnapshotLister) Get(name string) (*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass, error) {
|
||||||
|
for _, v := range f.values {
|
||||||
|
if v.Name == name {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdmitVolumeGroupSnapshotV1Alpha1(t *testing.T) {
|
||||||
|
selector := metav1.LabelSelector{MatchLabels: map[string]string{
|
||||||
|
"group": "A",
|
||||||
|
}}
|
||||||
|
mutatedField := "changed-immutable-field"
|
||||||
|
contentname := "groupsnapcontent1"
|
||||||
|
emptyVolumeGroupSnapshotClassName := ""
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
volumeGroupSnapshot *volumegroupsnapshotv1alpha1.VolumeGroupSnapshot
|
||||||
|
oldVolumeGroupSnapshot *volumegroupsnapshotv1alpha1.VolumeGroupSnapshot
|
||||||
|
shouldAdmit bool
|
||||||
|
msg string
|
||||||
|
operation v1.Operation
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Delete: new and old are nil. Should admit",
|
||||||
|
volumeGroupSnapshot: nil,
|
||||||
|
oldVolumeGroupSnapshot: nil,
|
||||||
|
shouldAdmit: true,
|
||||||
|
operation: v1.Delete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Create: old is nil and new is valid, with contentname",
|
||||||
|
volumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldVolumeGroupSnapshot: nil,
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Create,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Create: old is nil and new is valid, with selector",
|
||||||
|
volumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
Selector: selector,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldVolumeGroupSnapshot: nil,
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Create,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is valid and new is invalid",
|
||||||
|
volumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
},
|
||||||
|
VolumeGroupSnapshotClassName: &emptyVolumeGroupSnapshotClassName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldVolumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldAdmit: false,
|
||||||
|
operation: v1.Update,
|
||||||
|
msg: "Spec.VolumeGroupSnapshotClassName must not be the empty string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is valid and new is valid",
|
||||||
|
volumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldVolumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldAdmit: true,
|
||||||
|
operation: v1.Update,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is valid and new is valid but changes immutable field spec.source",
|
||||||
|
volumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &mutatedField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldVolumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldAdmit: false,
|
||||||
|
operation: v1.Update,
|
||||||
|
msg: fmt.Sprintf("Spec.Source.VolumeGroupSnapshotContentName is immutable but was changed from %s to %s", contentname, mutatedField),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is invalid and new is valid",
|
||||||
|
volumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldVolumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
Selector: selector,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldAdmit: false,
|
||||||
|
operation: v1.Update,
|
||||||
|
msg: fmt.Sprintf("Spec.Source.Selector is immutable but was changed from %v to %v", selector, metav1.LabelSelector{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// will be handled by schema validation
|
||||||
|
name: "Update: old is invalid and new is invalid",
|
||||||
|
volumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
Selector: selector,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldVolumeGroupSnapshot: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshot{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotSource{
|
||||||
|
VolumeGroupSnapshotContentName: &contentname,
|
||||||
|
Selector: selector,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldAdmit: true,
|
||||||
|
operation: v1.Update,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
groupSnapshot := tc.volumeGroupSnapshot
|
||||||
|
raw, err := json.Marshal(groupSnapshot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldGroupSnapshot := tc.oldVolumeGroupSnapshot
|
||||||
|
oldRaw, err := json.Marshal(oldGroupSnapshot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
review := v1.AdmissionReview{
|
||||||
|
Request: &v1.AdmissionRequest{
|
||||||
|
Object: runtime.RawExtension{
|
||||||
|
Raw: raw,
|
||||||
|
},
|
||||||
|
OldObject: runtime.RawExtension{
|
||||||
|
Raw: oldRaw,
|
||||||
|
},
|
||||||
|
Resource: GroupSnapshotV1Alpha1GVR,
|
||||||
|
Operation: tc.operation,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sa := NewGroupSnapshotAdmitter(nil)
|
||||||
|
response := sa.Admit(review)
|
||||||
|
shouldAdmit := response.Allowed
|
||||||
|
msg := response.Result.Message
|
||||||
|
|
||||||
|
expectedResponse := tc.shouldAdmit
|
||||||
|
expectedMsg := tc.msg
|
||||||
|
|
||||||
|
if shouldAdmit != expectedResponse {
|
||||||
|
t.Errorf("expected \"%v\" to equal \"%v\": %v", shouldAdmit, expectedResponse, msg)
|
||||||
|
}
|
||||||
|
if msg != expectedMsg {
|
||||||
|
t.Errorf("expected \"%v\" to equal \"%v\"", msg, expectedMsg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdmitVolumeGroupSnapshotContentV1Alpha1(t *testing.T) {
|
||||||
|
volumeHandle := "volumeHandle1"
|
||||||
|
modifiedField := "modified-field"
|
||||||
|
groupSnapshotHandle := "groupsnapshotHandle1"
|
||||||
|
volumeGroupSnapshotClassName := "volume-snapshot-class-1"
|
||||||
|
validContent := &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContentSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContentSource{
|
||||||
|
VolumeGroupSnapshotHandle: &groupSnapshotHandle,
|
||||||
|
},
|
||||||
|
VolumeGroupSnapshotRef: core_v1.ObjectReference{
|
||||||
|
Name: "group-snapshot-ref",
|
||||||
|
Namespace: "default-ns",
|
||||||
|
},
|
||||||
|
VolumeGroupSnapshotClassName: &volumeGroupSnapshotClassName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
invalidContent := &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContentSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContentSource{
|
||||||
|
VolumeGroupSnapshotHandle: &groupSnapshotHandle,
|
||||||
|
PersistentVolumeNames: []string{volumeHandle},
|
||||||
|
},
|
||||||
|
VolumeGroupSnapshotRef: core_v1.ObjectReference{
|
||||||
|
Name: "",
|
||||||
|
Namespace: "default-ns",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
groupSnapContent *volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent
|
||||||
|
oldGroupSnapContent *volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent
|
||||||
|
shouldAdmit bool
|
||||||
|
msg string
|
||||||
|
operation v1.Operation
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Delete: both new and old are nil",
|
||||||
|
groupSnapContent: nil,
|
||||||
|
oldGroupSnapContent: nil,
|
||||||
|
shouldAdmit: true,
|
||||||
|
operation: v1.Delete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Create: old is nil and new is valid",
|
||||||
|
groupSnapContent: validContent,
|
||||||
|
oldGroupSnapContent: nil,
|
||||||
|
shouldAdmit: true,
|
||||||
|
operation: v1.Create,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is valid and new is invalid",
|
||||||
|
groupSnapContent: invalidContent,
|
||||||
|
oldGroupSnapContent: validContent,
|
||||||
|
shouldAdmit: false,
|
||||||
|
operation: v1.Update,
|
||||||
|
msg: fmt.Sprintf("Spec.Source.PersistentVolumeNames is immutable but was changed from %s to %s", []string{}, []string{volumeHandle}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is valid and new is valid",
|
||||||
|
groupSnapContent: validContent,
|
||||||
|
oldGroupSnapContent: validContent,
|
||||||
|
shouldAdmit: true,
|
||||||
|
operation: v1.Update,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is valid and new is valid but modifies immutable field",
|
||||||
|
groupSnapContent: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContentSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContentSource{
|
||||||
|
VolumeGroupSnapshotHandle: &modifiedField,
|
||||||
|
},
|
||||||
|
VolumeGroupSnapshotRef: core_v1.ObjectReference{
|
||||||
|
Name: "snapshot-ref",
|
||||||
|
Namespace: "default-ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldGroupSnapContent: validContent,
|
||||||
|
shouldAdmit: false,
|
||||||
|
operation: v1.Update,
|
||||||
|
msg: fmt.Sprintf("Spec.Source.VolumeGroupSnapshotHandle is immutable but was changed from %s to %s", groupSnapshotHandle, modifiedField),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is valid and new is valid but modifies immutable ref",
|
||||||
|
groupSnapContent: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent{
|
||||||
|
Spec: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContentSpec{
|
||||||
|
Source: volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContentSource{
|
||||||
|
VolumeGroupSnapshotHandle: &groupSnapshotHandle,
|
||||||
|
},
|
||||||
|
VolumeGroupSnapshotRef: core_v1.ObjectReference{
|
||||||
|
Name: modifiedField,
|
||||||
|
Namespace: "default-ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldGroupSnapContent: validContent,
|
||||||
|
shouldAdmit: false,
|
||||||
|
operation: v1.Update,
|
||||||
|
msg: fmt.Sprintf("Spec.VolumeGroupSnapshotRef.Name is immutable but was changed from %s to %s",
|
||||||
|
validContent.Spec.VolumeGroupSnapshotRef.Name, modifiedField),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is invalid and new is valid",
|
||||||
|
groupSnapContent: validContent,
|
||||||
|
oldGroupSnapContent: invalidContent,
|
||||||
|
shouldAdmit: false,
|
||||||
|
operation: v1.Update,
|
||||||
|
msg: fmt.Sprintf("Spec.Source.PersistentVolumeNames is immutable but was changed from %s to %s", []string{volumeHandle}, []string{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update: old is invalid and new is invalid",
|
||||||
|
groupSnapContent: invalidContent,
|
||||||
|
oldGroupSnapContent: invalidContent,
|
||||||
|
shouldAdmit: false,
|
||||||
|
operation: v1.Update,
|
||||||
|
msg: "both Spec.VolumeGroupSnapshotRef.Name = and Spec.VolumeGroupSnapshotRef.Namespace = default-ns must be set",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
groupSnapContent := tc.groupSnapContent
|
||||||
|
raw, err := json.Marshal(groupSnapContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldGroupSnapContent := tc.oldGroupSnapContent
|
||||||
|
oldRaw, err := json.Marshal(oldGroupSnapContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
review := v1.AdmissionReview{
|
||||||
|
Request: &v1.AdmissionRequest{
|
||||||
|
Object: runtime.RawExtension{
|
||||||
|
Raw: raw,
|
||||||
|
},
|
||||||
|
OldObject: runtime.RawExtension{
|
||||||
|
Raw: oldRaw,
|
||||||
|
},
|
||||||
|
Resource: GroupSnapshotContentV1Apha1GVR,
|
||||||
|
Operation: tc.operation,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sa := NewGroupSnapshotAdmitter(nil)
|
||||||
|
response := sa.Admit(review)
|
||||||
|
shouldAdmit := response.Allowed
|
||||||
|
msg := response.Result.Message
|
||||||
|
|
||||||
|
expectedResponse := tc.shouldAdmit
|
||||||
|
expectedMsg := tc.msg
|
||||||
|
|
||||||
|
if shouldAdmit != expectedResponse {
|
||||||
|
t.Errorf("expected \"%v\" to equal \"%v\"", shouldAdmit, expectedResponse)
|
||||||
|
}
|
||||||
|
if msg != expectedMsg {
|
||||||
|
t.Errorf("expected \"%v\" to equal \"%v\"", msg, expectedMsg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdmitVolumeGroupSnapshotClassV1Alpha1(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
groupSnapClass *volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass
|
||||||
|
oldGroupSnapClass *volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass
|
||||||
|
shouldAdmit bool
|
||||||
|
msg string
|
||||||
|
operation v1.Operation
|
||||||
|
lister groupsnapshotlisters.VolumeGroupSnapshotClassLister
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "new default for group snapshot class with no existing group snapshot classes",
|
||||||
|
groupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldGroupSnapClass: nil,
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeGroupSnapshotLister{values: []*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new default for group snapshot class for with existing default group snapshot class with different drivers",
|
||||||
|
groupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldGroupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{},
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeGroupSnapshotLister{values: []*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "existing.test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new default for group snapshot class with existing default group snapshot class same driver",
|
||||||
|
groupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldGroupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{},
|
||||||
|
shouldAdmit: false,
|
||||||
|
msg: "default group snapshot class: driver-a already exists for driver: test.csi.io",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeGroupSnapshotLister{values: []*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "driver-a",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default for group snapshot class with existing default group snapshot class same driver update",
|
||||||
|
groupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldGroupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Update,
|
||||||
|
lister: &fakeGroupSnapshotLister{values: []*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new group snapshot for group snapshot class with existing default group snapshot class same driver",
|
||||||
|
groupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldGroupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{},
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeGroupSnapshotLister{values: []*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new group snapshot for group snapshot class with existing group snapshot default classes",
|
||||||
|
groupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldGroupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{},
|
||||||
|
shouldAdmit: false,
|
||||||
|
msg: "default group snapshot class: driver-is-default already exists for driver: test.csi.io",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeGroupSnapshotLister{[]*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "driver-is-default",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update group snapshot class to new driver with existing default group snapshot classes",
|
||||||
|
groupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "driver.test.csi.io",
|
||||||
|
},
|
||||||
|
oldGroupSnapClass: &volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
shouldAdmit: false,
|
||||||
|
msg: "default group snapshot class: driver-test-default already exists for driver: driver.test.csi.io",
|
||||||
|
operation: v1.Update,
|
||||||
|
lister: &fakeGroupSnapshotLister{values: []*volumegroupsnapshotv1alpha1.VolumeGroupSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "driver-is-default",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "driver-test-default",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultGroupSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "driver.test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
groupSnapContent := tc.groupSnapClass
|
||||||
|
raw, err := json.Marshal(groupSnapContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldGroupSnapClass := tc.oldGroupSnapClass
|
||||||
|
oldRaw, err := json.Marshal(oldGroupSnapClass)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
review := v1.AdmissionReview{
|
||||||
|
Request: &v1.AdmissionRequest{
|
||||||
|
Object: runtime.RawExtension{
|
||||||
|
Raw: raw,
|
||||||
|
},
|
||||||
|
OldObject: runtime.RawExtension{
|
||||||
|
Raw: oldRaw,
|
||||||
|
},
|
||||||
|
Resource: GroupSnapshotClassV1Apha1GVR,
|
||||||
|
Operation: tc.operation,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sa := NewGroupSnapshotAdmitter(tc.lister)
|
||||||
|
response := sa.Admit(review)
|
||||||
|
|
||||||
|
shouldAdmit := response.Allowed
|
||||||
|
msg := response.Result.Message
|
||||||
|
|
||||||
|
expectedResponse := tc.shouldAdmit
|
||||||
|
expectedMsg := tc.msg
|
||||||
|
|
||||||
|
if shouldAdmit != expectedResponse {
|
||||||
|
t.Errorf("expected \"%v\" to equal \"%v\"", shouldAdmit, expectedResponse)
|
||||||
|
}
|
||||||
|
if msg != expectedMsg {
|
||||||
|
t.Errorf("expected \"%v\" to equal \"%v\"", msg, expectedMsg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -109,7 +109,8 @@ func (a admitter) Admit(ar v1.AdmissionReview) *v1.AdmissionResponse {
|
|||||||
}
|
}
|
||||||
return decideSnapshotClassV1(snapClass, oldSnapClass, a.lister)
|
return decideSnapshotClassV1(snapClass, oldSnapClass, a.lister)
|
||||||
default:
|
default:
|
||||||
err := fmt.Errorf("expect resource to be %s, %s or %s", SnapshotV1GVR, SnapshotContentV1GVR, SnapshotClassV1GVR)
|
err := fmt.Errorf("expect resource to be %s, %s, or %s, but found %v",
|
||||||
|
SnapshotV1GVR, SnapshotContentV1GVR, SnapshotClassV1GVR, ar.Request.Resource)
|
||||||
klog.Error(err)
|
klog.Error(err)
|
||||||
return toV1AdmissionResponse(err)
|
return toV1AdmissionResponse(err)
|
||||||
}
|
}
|
||||||
|
@@ -314,7 +314,7 @@ func TestAdmitVolumeSnapshotContentV1(t *testing.T) {
|
|||||||
oldVolumeSnapshotContent: invalidContent,
|
oldVolumeSnapshotContent: invalidContent,
|
||||||
shouldAdmit: false,
|
shouldAdmit: false,
|
||||||
operation: v1.Update,
|
operation: v1.Update,
|
||||||
msg: fmt.Sprintf("both Spec.VolumeSnapshotRef.Name = and Spec.VolumeSnapshotRef.Namespace = default-ns must be set"),
|
msg: "both Spec.VolumeSnapshotRef.Name = and Spec.VolumeSnapshotRef.Namespace = default-ns must be set",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ package webhook
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
groupsnapshotcrdv1alpha1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumegroupsnapshot/v1alpha1"
|
||||||
crdv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
|
crdv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,3 +54,35 @@ func ValidateV1SnapshotContent(snapcontent *crdv1.VolumeSnapshotContent) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateV1Alpha1GroupSnapshotContent performs additional strict validation.
|
||||||
|
// Do NOT rely on this function to fully validate group snapshot content objects.
|
||||||
|
// This function will only check the additional rules provided by the webhook.
|
||||||
|
func ValidateV1Alpha1GroupSnapshotContent(groupSnapcontent *groupsnapshotcrdv1alpha1.VolumeGroupSnapshotContent) error {
|
||||||
|
if groupSnapcontent == nil {
|
||||||
|
return fmt.Errorf("VolumeGroupSnapshotContent is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
vgsref := groupSnapcontent.Spec.VolumeGroupSnapshotRef
|
||||||
|
|
||||||
|
if vgsref.Name == "" || vgsref.Namespace == "" {
|
||||||
|
return fmt.Errorf("both Spec.VolumeGroupSnapshotRef.Name = %s and Spec.VolumeGroupSnapshotRef.Namespace = %s must be set", vgsref.Name, vgsref.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateV1Alpha1GroupSnapshot performs additional strict validation.
|
||||||
|
// Do NOT rely on this function to fully validate group snapshot objects.
|
||||||
|
// This function will only check the additional rules provided by the webhook.
|
||||||
|
func ValidateV1Alpha1GroupSnapshot(snapshot *groupsnapshotcrdv1alpha1.VolumeGroupSnapshot) error {
|
||||||
|
if snapshot == nil {
|
||||||
|
return fmt.Errorf("VolumeGroupSnapshot is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
vgscname := snapshot.Spec.VolumeGroupSnapshotClassName
|
||||||
|
if vgscname != nil && *vgscname == "" {
|
||||||
|
return fmt.Errorf("Spec.VolumeGroupSnapshotClassName must not be the empty string")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -26,7 +26,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
clientset "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned"
|
clientset "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned"
|
||||||
storagelisters "github.com/kubernetes-csi/external-snapshotter/client/v6/listers/volumesnapshot/v1"
|
groupsnapshotlisters "github.com/kubernetes-csi/external-snapshotter/client/v6/listers/volumegroupsnapshot/v1alpha1"
|
||||||
|
snapshotlisters "github.com/kubernetes-csi/external-snapshotter/client/v6/listers/volumesnapshot/v1"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
informers "github.com/kubernetes-csi/external-snapshotter/client/v6/informers/externalversions"
|
informers "github.com/kubernetes-csi/external-snapshotter/client/v6/informers/externalversions"
|
||||||
@@ -183,15 +184,29 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type serveWebhook struct {
|
type serveSnapshotWebhook struct {
|
||||||
lister storagelisters.VolumeSnapshotClassLister
|
lister snapshotlisters.VolumeSnapshotClassLister
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s serveWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s serveSnapshotWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
serve(w, r, newDelegateToV1AdmitHandler(NewSnapshotAdmitter(s.lister)))
|
serve(w, r, newDelegateToV1AdmitHandler(NewSnapshotAdmitter(s.lister)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher, lister storagelisters.VolumeSnapshotClassLister) error {
|
type serveGroupSnapshotWebhook struct {
|
||||||
|
lister groupsnapshotlisters.VolumeGroupSnapshotClassLister
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s serveGroupSnapshotWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serve(w, r, newDelegateToV1AdmitHandler(NewGroupSnapshotAdmitter(s.lister)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer(
|
||||||
|
ctx context.Context,
|
||||||
|
tlsConfig *tls.Config,
|
||||||
|
cw *CertWatcher,
|
||||||
|
vscLister snapshotlisters.VolumeSnapshotClassLister,
|
||||||
|
vgscLister groupsnapshotlisters.VolumeGroupSnapshotClassLister,
|
||||||
|
) error {
|
||||||
go func() {
|
go func() {
|
||||||
klog.Info("Starting certificate watcher")
|
klog.Info("Starting certificate watcher")
|
||||||
if err := cw.Start(ctx); err != nil {
|
if err := cw.Start(ctx); err != nil {
|
||||||
@@ -199,13 +214,17 @@ func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher, li
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
// Pipe through the informer at some point here.
|
// Pipe through the informer at some point here.
|
||||||
s := &serveWebhook{
|
snapshotWebhook := serveSnapshotWebhook{
|
||||||
lister: lister,
|
lister: vscLister,
|
||||||
|
}
|
||||||
|
groupSnapshotWebhook := serveGroupSnapshotWebhook{
|
||||||
|
lister: vgscLister,
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Starting webhook server")
|
fmt.Println("Starting webhook server")
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/volumesnapshot", s)
|
mux.Handle("/volumesnapshot", snapshotWebhook)
|
||||||
|
mux.Handle("/volumegroupsnapshot", groupSnapshotWebhook)
|
||||||
mux.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) })
|
mux.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) })
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
@@ -247,14 +266,15 @@ func main(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory := informers.NewSharedInformerFactory(snapClient, 0)
|
factory := informers.NewSharedInformerFactory(snapClient, 0)
|
||||||
lister := factory.Snapshot().V1().VolumeSnapshotClasses().Lister()
|
snapshotLister := factory.Snapshot().V1().VolumeSnapshotClasses().Lister()
|
||||||
|
groupSnapshotLister := factory.Groupsnapshot().V1alpha1().VolumeGroupSnapshotClasses().Lister()
|
||||||
|
|
||||||
// Start the informers
|
// Start the informers
|
||||||
factory.Start(ctx.Done())
|
factory.Start(ctx.Done())
|
||||||
// wait for the caches to sync
|
// wait for the caches to sync
|
||||||
factory.WaitForCacheSync(ctx.Done())
|
factory.WaitForCacheSync(ctx.Done())
|
||||||
|
|
||||||
if err := startServer(ctx, tlsConfig, cw, lister); err != nil {
|
if err := startServer(ctx, tlsConfig, cw, snapshotLister, groupSnapshotLister); err != nil {
|
||||||
klog.Fatalf("server stopped: %v", err)
|
klog.Fatalf("server stopped: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,10 @@ func TestWebhookCertReload(t *testing.T) {
|
|||||||
t.Errorf("unexpected error occurred while deleting certs: %v", err)
|
t.Errorf("unexpected error occurred while deleting certs: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
generateTestCertKeyPair(t, certFile, keyFile)
|
err = generateTestCertKeyPair(t, certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error occurred while generating test certs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Start test server
|
// Start test server
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -45,7 +48,12 @@ func TestWebhookCertReload(t *testing.T) {
|
|||||||
GetCertificate: cw.GetCertificate,
|
GetCertificate: cw.GetCertificate,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err := startServer(ctx, tlsConfig, cw, &fakeSnapshotLister{}); err != nil {
|
err := startServer(ctx,
|
||||||
|
tlsConfig,
|
||||||
|
cw,
|
||||||
|
&fakeSnapshotLister{},
|
||||||
|
&fakeGroupSnapshotLister{})
|
||||||
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -74,7 +82,10 @@ func TestWebhookCertReload(t *testing.T) {
|
|||||||
// TC: Certificate should consistently change with a file change
|
// TC: Certificate should consistently change with a file change
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
// Generate new key/cert
|
// Generate new key/cert
|
||||||
generateTestCertKeyPair(t, certFile, keyFile)
|
err = generateTestCertKeyPair(t, certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error occurred while generating test certs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for certwatcher to update
|
// Wait for certwatcher to update
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
Reference in New Issue
Block a user