Merge pull request #674 from shawn-hurley/add-default-validation
Add default validation for VolumeSnapshotClass
This commit is contained in:
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package webhook
|
package webhook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
snapshot "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned/scheme"
|
||||||
admissionv1 "k8s.io/api/admission/v1"
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||||
@@ -40,4 +41,5 @@ func addToScheme(scheme *runtime.Scheme) {
|
|||||||
utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme))
|
utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme))
|
||||||
utilruntime.Must(admissionv1.AddToScheme(scheme))
|
utilruntime.Must(admissionv1.AddToScheme(scheme))
|
||||||
utilruntime.Must(admissionregistrationv1.AddToScheme(scheme))
|
utilruntime.Must(admissionregistrationv1.AddToScheme(scheme))
|
||||||
|
utilruntime.Must(snapshot.AddToScheme(scheme))
|
||||||
}
|
}
|
||||||
|
@@ -22,8 +22,11 @@ import (
|
|||||||
|
|
||||||
volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
|
volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
|
||||||
volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1beta1"
|
volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1beta1"
|
||||||
|
storagelisters "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"
|
v1 "k8s.io/api/admission/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,10 +39,26 @@ var (
|
|||||||
SnapshotContentV1Beta1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1beta1.GroupName, Version: "v1beta1", Resource: "volumesnapshotcontents"}
|
SnapshotContentV1Beta1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1beta1.GroupName, Version: "v1beta1", Resource: "volumesnapshotcontents"}
|
||||||
// SnapshotContentV1GVR is GroupVersionResource for v1 VolumeSnapshotContents
|
// SnapshotContentV1GVR is GroupVersionResource for v1 VolumeSnapshotContents
|
||||||
SnapshotContentV1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1.GroupName, Version: "v1", Resource: "volumesnapshotcontents"}
|
SnapshotContentV1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1.GroupName, Version: "v1", Resource: "volumesnapshotcontents"}
|
||||||
|
// SnapshotContentV1GVR is GroupVersionResource for v1 VolumeSnapshotContents
|
||||||
|
SnapshotClassV1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1.GroupName, Version: "v1", Resource: "volumesnapshotclasses"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SnapshotAdmitter interface {
|
||||||
|
Admit(v1.AdmissionReview) *v1.AdmissionResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
type admitter struct {
|
||||||
|
lister storagelisters.VolumeSnapshotClassLister
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSnapshotAdmitter(lister storagelisters.VolumeSnapshotClassLister) SnapshotAdmitter {
|
||||||
|
return &admitter{
|
||||||
|
lister: lister,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add a label {"added-label": "yes"} to the object
|
// Add a label {"added-label": "yes"} to the object
|
||||||
func admitSnapshot(ar v1.AdmissionReview) *v1.AdmissionResponse {
|
func (a admitter) Admit(ar v1.AdmissionReview) *v1.AdmissionResponse {
|
||||||
klog.V(2).Info("admitting volumesnapshots or volumesnapshotcontents")
|
klog.V(2).Info("admitting volumesnapshots or volumesnapshotcontents")
|
||||||
|
|
||||||
reviewResponse := &v1.AdmissionResponse{
|
reviewResponse := &v1.AdmissionResponse{
|
||||||
@@ -106,6 +125,18 @@ func admitSnapshot(ar v1.AdmissionReview) *v1.AdmissionResponse {
|
|||||||
return toV1AdmissionResponse(err)
|
return toV1AdmissionResponse(err)
|
||||||
}
|
}
|
||||||
return decideSnapshotContentV1(snapcontent, oldSnapcontent, isUpdate)
|
return decideSnapshotContentV1(snapcontent, oldSnapcontent, isUpdate)
|
||||||
|
case SnapshotClassV1GVR:
|
||||||
|
snapClass := &volumesnapshotv1.VolumeSnapshotClass{}
|
||||||
|
if _, _, err := deserializer.Decode(raw, nil, snapClass); err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
oldSnapClass := &volumesnapshotv1.VolumeSnapshotClass{}
|
||||||
|
if _, _, err := deserializer.Decode(oldRaw, nil, oldSnapClass); err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return toV1AdmissionResponse(err)
|
||||||
|
}
|
||||||
|
return decideSnapshotClassV1(snapClass, oldSnapClass, a.lister)
|
||||||
default:
|
default:
|
||||||
err := fmt.Errorf("expect resource to be %s or %s", SnapshotV1Beta1GVR, SnapshotContentV1Beta1GVR)
|
err := fmt.Errorf("expect resource to be %s or %s", SnapshotV1Beta1GVR, SnapshotContentV1Beta1GVR)
|
||||||
klog.Error(err)
|
klog.Error(err)
|
||||||
@@ -223,6 +254,43 @@ func decideSnapshotContentV1(snapcontent, oldSnapcontent *volumesnapshotv1.Volum
|
|||||||
return reviewResponse
|
return reviewResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decideSnapshotClassV1(snapClass, oldSnapClass *volumesnapshotv1.VolumeSnapshotClass, lister storagelisters.VolumeSnapshotClassLister) *v1.AdmissionResponse {
|
||||||
|
reviewResponse := &v1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
Result: &metav1.Status{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only Validate when a new snapClass is being set as a default.
|
||||||
|
if snapClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] != "true" {
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Old 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 snapshot class: %v already exits for driver: %v", snapshotClass.Name, snapClass.Driver)
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
func strPtrDereference(s *string) string {
|
func strPtrDereference(s *string) string {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return "<nil string pointer>"
|
return "<nil string pointer>"
|
||||||
|
@@ -23,8 +23,12 @@ import (
|
|||||||
|
|
||||||
volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
|
volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
|
||||||
volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1beta1"
|
volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1beta1"
|
||||||
|
storagelisters "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"
|
v1 "k8s.io/api/admission/v1"
|
||||||
core_v1 "k8s.io/api/core/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"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -208,7 +212,8 @@ func TestAdmitVolumeSnapshotV1beta1(t *testing.T) {
|
|||||||
Operation: tc.operation,
|
Operation: tc.operation,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
response := admitSnapshot(review)
|
sa := NewSnapshotAdmitter(nil)
|
||||||
|
response := sa.Admit(review)
|
||||||
shouldAdmit := response.Allowed
|
shouldAdmit := response.Allowed
|
||||||
msg := response.Result.Message
|
msg := response.Result.Message
|
||||||
|
|
||||||
@@ -391,7 +396,8 @@ func TestAdmitVolumeSnapshotV1(t *testing.T) {
|
|||||||
Operation: tc.operation,
|
Operation: tc.operation,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
response := admitSnapshot(review)
|
sa := NewSnapshotAdmitter(nil)
|
||||||
|
response := sa.Admit(review)
|
||||||
shouldAdmit := response.Allowed
|
shouldAdmit := response.Allowed
|
||||||
msg := response.Result.Message
|
msg := response.Result.Message
|
||||||
|
|
||||||
@@ -541,7 +547,8 @@ func TestAdmitVolumeSnapshotContentV1beta1(t *testing.T) {
|
|||||||
Operation: tc.operation,
|
Operation: tc.operation,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
response := admitSnapshot(review)
|
sa := NewSnapshotAdmitter(nil)
|
||||||
|
response := sa.Admit(review)
|
||||||
shouldAdmit := response.Allowed
|
shouldAdmit := response.Allowed
|
||||||
msg := response.Result.Message
|
msg := response.Result.Message
|
||||||
|
|
||||||
@@ -685,7 +692,293 @@ func TestAdmitVolumeSnapshotContentV1(t *testing.T) {
|
|||||||
Operation: tc.operation,
|
Operation: tc.operation,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
response := admitSnapshot(review)
|
sa := NewSnapshotAdmitter(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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeSnapshotLister struct {
|
||||||
|
values []*volumesnapshotv1.VolumeSnapshotClass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeSnapshotLister) List(selector labels.Selector) (ret []*volumesnapshotv1.VolumeSnapshotClass, err error) {
|
||||||
|
return f.values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeSnapshotLister) Get(name string) (*volumesnapshotv1.VolumeSnapshotClass, error) {
|
||||||
|
for _, v := range f.values {
|
||||||
|
if v.Name == name {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdmitVolumeSnapshotClassV1(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
volumeSnapshotClass *volumesnapshotv1.VolumeSnapshotClass
|
||||||
|
oldVolumeSnapshotClass *volumesnapshotv1.VolumeSnapshotClass
|
||||||
|
shouldAdmit bool
|
||||||
|
msg string
|
||||||
|
operation v1.Operation
|
||||||
|
lister storagelisters.VolumeSnapshotClassLister
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "new default for class with no existing classes",
|
||||||
|
volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new default for class for with existing default class different drivers",
|
||||||
|
volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "existing.test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new default for class with existing default class same driver",
|
||||||
|
volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
|
||||||
|
shouldAdmit: false,
|
||||||
|
msg: "default snapshot class: driver-a already exits for driver: test.csi.io",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "driver-a",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default for class with existing default class same driver update",
|
||||||
|
volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Update,
|
||||||
|
lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new snapshot for class with existing default class same driver",
|
||||||
|
volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
|
||||||
|
shouldAdmit: true,
|
||||||
|
msg: "",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new snapshot for class with existing default classes",
|
||||||
|
volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
|
||||||
|
shouldAdmit: false,
|
||||||
|
msg: "default snapshot class: driver-is-default already exits for driver: test.csi.io",
|
||||||
|
operation: v1.Create,
|
||||||
|
lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "driver-is-default",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update snapshot class to new driver with existing default classes",
|
||||||
|
volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "driver.test.csi.io",
|
||||||
|
},
|
||||||
|
oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
shouldAdmit: false,
|
||||||
|
msg: "default snapshot class: driver-test-default already exits for driver: driver.test.csi.io",
|
||||||
|
operation: v1.Update,
|
||||||
|
lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "driver-is-default",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "test.csi.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "driver-test-default",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
utils.IsDefaultSnapshotClassAnnotation: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Driver: "driver.test.csi.io",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
snapshotContent := tc.volumeSnapshotClass
|
||||||
|
raw, err := json.Marshal(snapshotContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldSnapshotClass := tc.oldVolumeSnapshotClass
|
||||||
|
oldRaw, err := json.Marshal(oldSnapshotClass)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
review := v1.AdmissionReview{
|
||||||
|
Request: &v1.AdmissionRequest{
|
||||||
|
Object: runtime.RawExtension{
|
||||||
|
Raw: raw,
|
||||||
|
},
|
||||||
|
OldObject: runtime.RawExtension{
|
||||||
|
Raw: oldRaw,
|
||||||
|
},
|
||||||
|
Resource: SnapshotClassV1GVR,
|
||||||
|
Operation: tc.operation,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sa := NewSnapshotAdmitter(tc.lister)
|
||||||
|
response := sa.Admit(review)
|
||||||
|
|
||||||
shouldAdmit := response.Allowed
|
shouldAdmit := response.Allowed
|
||||||
msg := response.Result.Message
|
msg := response.Result.Message
|
||||||
|
|
||||||
|
@@ -23,19 +23,26 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
clientset "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned"
|
||||||
|
storagelisters "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"
|
||||||
v1 "k8s.io/api/admission/v1"
|
v1 "k8s.io/api/admission/v1"
|
||||||
"k8s.io/api/admission/v1beta1"
|
"k8s.io/api/admission/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
certFile string
|
certFile string
|
||||||
keyFile string
|
keyFile string
|
||||||
port int
|
kubeconfigFile string
|
||||||
|
port int
|
||||||
)
|
)
|
||||||
|
|
||||||
// CmdWebhook is used by Cobra.
|
// CmdWebhook is used by Cobra.
|
||||||
@@ -58,6 +65,8 @@ func init() {
|
|||||||
"Secure port that the webhook listens on")
|
"Secure port that the webhook listens on")
|
||||||
CmdWebhook.MarkFlagRequired("tls-cert-file")
|
CmdWebhook.MarkFlagRequired("tls-cert-file")
|
||||||
CmdWebhook.MarkFlagRequired("tls-private-key-file")
|
CmdWebhook.MarkFlagRequired("tls-private-key-file")
|
||||||
|
// Add optional flag for kubeconfig
|
||||||
|
CmdWebhook.Flags().StringVar(&kubeconfigFile, "kubeconfig", "", "kubeconfig file to use for volumesnapshotclasses")
|
||||||
}
|
}
|
||||||
|
|
||||||
// admitv1beta1Func handles a v1beta1 admission
|
// admitv1beta1Func handles a v1beta1 admission
|
||||||
@@ -68,14 +77,12 @@ type admitv1Func func(v1.AdmissionReview) *v1.AdmissionResponse
|
|||||||
|
|
||||||
// admitHandler is a handler, for both validators and mutators, that supports multiple admission review versions
|
// admitHandler is a handler, for both validators and mutators, that supports multiple admission review versions
|
||||||
type admitHandler struct {
|
type admitHandler struct {
|
||||||
v1beta1 admitv1beta1Func
|
SnapshotAdmitter
|
||||||
v1 admitv1Func
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDelegateToV1AdmitHandler(f admitv1Func) admitHandler {
|
func newDelegateToV1AdmitHandler(sa SnapshotAdmitter) admitHandler {
|
||||||
return admitHandler{
|
return admitHandler{
|
||||||
v1beta1: delegateV1beta1AdmitToV1(f),
|
SnapshotAdmitter: sa,
|
||||||
v1: f,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +144,7 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
|
|||||||
}
|
}
|
||||||
responseAdmissionReview := &v1beta1.AdmissionReview{}
|
responseAdmissionReview := &v1beta1.AdmissionReview{}
|
||||||
responseAdmissionReview.SetGroupVersionKind(*gvk)
|
responseAdmissionReview.SetGroupVersionKind(*gvk)
|
||||||
responseAdmissionReview.Response = admit.v1beta1(*requestedAdmissionReview)
|
responseAdmissionReview.Response = delegateV1beta1AdmitToV1(admit.Admit)(*requestedAdmissionReview)
|
||||||
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
|
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
|
||||||
responseObj = responseAdmissionReview
|
responseObj = responseAdmissionReview
|
||||||
case v1.SchemeGroupVersion.WithKind("AdmissionReview"):
|
case v1.SchemeGroupVersion.WithKind("AdmissionReview"):
|
||||||
@@ -150,7 +157,7 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
|
|||||||
}
|
}
|
||||||
responseAdmissionReview := &v1.AdmissionReview{}
|
responseAdmissionReview := &v1.AdmissionReview{}
|
||||||
responseAdmissionReview.SetGroupVersionKind(*gvk)
|
responseAdmissionReview.SetGroupVersionKind(*gvk)
|
||||||
responseAdmissionReview.Response = admit.v1(*requestedAdmissionReview)
|
responseAdmissionReview.Response = admit.Admit(*requestedAdmissionReview)
|
||||||
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
|
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
|
||||||
responseObj = responseAdmissionReview
|
responseObj = responseAdmissionReview
|
||||||
default:
|
default:
|
||||||
@@ -173,21 +180,29 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveSnapshotRequest(w http.ResponseWriter, r *http.Request) {
|
type serveWebhook struct {
|
||||||
serve(w, r, newDelegateToV1AdmitHandler(admitSnapshot))
|
lister storagelisters.VolumeSnapshotClassLister
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher) error {
|
func (s serveWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serve(w, r, newDelegateToV1AdmitHandler(NewSnapshotAdmitter(s.lister)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher, lister storagelisters.VolumeSnapshotClassLister) 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 {
|
||||||
klog.Errorf("certificate watcher error: %v", err)
|
klog.Errorf("certificate watcher error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
// Pipe through the informer at some point here.
|
||||||
|
s := &serveWebhook{
|
||||||
|
lister: lister,
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Starting webhook server")
|
fmt.Println("Starting webhook server")
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/volumesnapshot", serveSnapshotRequest)
|
mux.Handle("/volumesnapshot", s)
|
||||||
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,
|
||||||
@@ -215,7 +230,35 @@ func main(cmd *cobra.Command, args []string) {
|
|||||||
GetCertificate: cw.GetCertificate,
|
GetCertificate: cw.GetCertificate,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := startServer(ctx, tlsConfig, cw); err != nil {
|
// Create an indexer.
|
||||||
|
// Create the client config. Use kubeconfig if given, otherwise assume in-cluster.
|
||||||
|
config, err := buildConfig(kubeconfigFile)
|
||||||
|
if err != nil {
|
||||||
|
klog.Error(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
snapClient, err := clientset.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
klog.Errorf("Error building snapshot clientset: %s", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
factory := informers.NewSharedInformerFactory(snapClient, 0)
|
||||||
|
lister := factory.Snapshot().V1().VolumeSnapshotClasses().Lister()
|
||||||
|
|
||||||
|
//Start the informers
|
||||||
|
factory.Start(ctx.Done())
|
||||||
|
//wait for the caches to sync
|
||||||
|
factory.WaitForCacheSync(ctx.Done())
|
||||||
|
|
||||||
|
if err := startServer(ctx, tlsConfig, cw, lister); err != nil {
|
||||||
klog.Fatalf("server stopped: %v", err)
|
klog.Fatalf("server stopped: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildConfig(kubeconfig string) (*rest.Config, error) {
|
||||||
|
if kubeconfig != "" {
|
||||||
|
return clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||||
|
}
|
||||||
|
return rest.InClusterConfig()
|
||||||
|
}
|
||||||
|
@@ -45,7 +45,7 @@ func TestWebhookCertReload(t *testing.T) {
|
|||||||
GetCertificate: cw.GetCertificate,
|
GetCertificate: cw.GetCertificate,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err := startServer(ctx, tlsConfig, cw); err != nil {
|
if err := startServer(ctx, tlsConfig, cw, &fakeSnapshotLister{}); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
Reference in New Issue
Block a user