Skip to content

Commit

Permalink
add groupsnapshot related webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Rakshith-R committed Mar 30, 2023
1 parent d30ffe6 commit 2f27cb0
Show file tree
Hide file tree
Showing 8 changed files with 986 additions and 19 deletions.
23 changes: 23 additions & 0 deletions deploy/kubernetes/webhook-example/admission-configuration-template
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,26 @@ webhooks:
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.
---
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.
3 changes: 3 additions & 0 deletions deploy/kubernetes/webhook-example/rbac-snapshot-webhook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ rules:
- apiGroups: ["snapshot.storage.k8s.io"]
resources: ["volumesnapshotclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["groupsnapshot.storage.k8s.io"]
resources: ["volumegroupsnapshotclasses"]
verbs: ["get", "list", "watch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
Expand Down
244 changes: 244 additions & 0 deletions pkg/validation-webhook/groupsnapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/*
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 VolumeSnapshots
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"}
// GroupSnapshotContentV1Apha1GVR is GroupVersionResource for v1alpha1 VolumeGroupSnapshotContents
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 oldSnapshot 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, oldSnapcontent *volumegroupsnapshotv1alpha1.VolumeGroupSnapshotContent, isUpdate bool) *v1.AdmissionResponse {
reviewResponse := &v1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{},
}

if isUpdate {
// if it is an UPDATE and oldSnapcontent is valid, check immutable fields
if err := checkGroupSnapshotContentImmutableFieldsV1Alpha1(groupSnapcontent, oldSnapcontent); 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(snapClass, oldSnapClass *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 snapClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] != "true" {
return reviewResponse
}

// If Old group snapshot class has this, then we can assume that it was validated if driver is the same.
if oldSnapClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] == "true" && oldSnapClass.Driver == snapClass.Driver {
return reviewResponse
}

ret, err := lister.List(labels.Everything())
if err != nil {
reviewResponse.Allowed = false
reviewResponse.Result.Message = err.Error()
return reviewResponse
}

for _, snapshotClass := range ret {
if snapshotClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] != "true" {
continue
}
if snapshotClass.Driver == snapClass.Driver {
reviewResponse.Allowed = false
reviewResponse.Result.Message = fmt.Sprintf("default group snapshot class: %v already exists for driver: %v", snapshotClass.Name, snapClass.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)
}

return nil
}
13 changes: 8 additions & 5 deletions pkg/validation-webhook/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import (
"reflect"

volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
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/kubernetes-csi/external-snapshotter/v6/pkg/utils"
v1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -43,10 +44,11 @@ type SnapshotAdmitter interface {
}

type admitter struct {
lister storagelisters.VolumeSnapshotClassLister
lister snapshotListers.VolumeSnapshotClassLister
groupSnapshotLister groupSnapshotListers.VolumeGroupSnapshotClassLister
}

func NewSnapshotAdmitter(lister storagelisters.VolumeSnapshotClassLister) SnapshotAdmitter {
func NewSnapshotAdmitter(lister snapshotListers.VolumeSnapshotClassLister) SnapshotAdmitter {
return &admitter{
lister: lister,
}
Expand Down Expand Up @@ -109,7 +111,8 @@ func (a admitter) Admit(ar v1.AdmissionReview) *v1.AdmissionResponse {
}
return decideSnapshotClassV1(snapClass, oldSnapClass, a.lister)
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)
return toV1AdmissionResponse(err)
}
Expand Down Expand Up @@ -161,7 +164,7 @@ func decideSnapshotContentV1(snapcontent, oldSnapcontent *volumesnapshotv1.Volum
return reviewResponse
}

func decideSnapshotClassV1(snapClass, oldSnapClass *volumesnapshotv1.VolumeSnapshotClass, lister storagelisters.VolumeSnapshotClassLister) *v1.AdmissionResponse {
func decideSnapshotClassV1(snapClass, oldSnapClass *volumesnapshotv1.VolumeSnapshotClass, lister snapshotListers.VolumeSnapshotClassLister) *v1.AdmissionResponse {
reviewResponse := &v1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{},
Expand Down
Loading

0 comments on commit 2f27cb0

Please sign in to comment.