Add phase 1 of validation tightening.

https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/177-volume-snapshot/tighten-validation-webhook-crd.md

1. Ratcheting validation webhook server image
2. Controller labels invalid objects
3. Unit tests for webhook
4. Deployment README and example deployment method with certs
5. Update top-level README

Racheting validation:
1. webhook is strict on create
2. webhook is strict on updates where the existing object passes strict validation
3. webhook is relaxed on updates where the existing object fails strict validation (allows finalizer removal, status update, deletion, etc)

Additionally the validating wehook server will perform immutability
checks on scenario 2 above.
This commit is contained in:
Andi Li
2020-08-04 18:55:54 +00:00
parent db336e8070
commit 42b6b374cf
73 changed files with 12815 additions and 21 deletions

View File

@@ -811,6 +811,15 @@ func newContent(contentName, boundToSnapshotUID, boundToSnapshotName, snapshotHa
return &content
}
func withSnapshotContentInvalidLabel(contents []*crdv1.VolumeSnapshotContent) []*crdv1.VolumeSnapshotContent {
for i := range contents {
if contents[i].ObjectMeta.Labels == nil {
contents[i].ObjectMeta.Labels = make(map[string]string)
}
contents[i].ObjectMeta.Labels[utils.VolumeSnapshotContentInvalidLabel] = ""
}
return contents
}
func withContentAnnotations(contents []*crdv1.VolumeSnapshotContent, annotations map[string]string) []*crdv1.VolumeSnapshotContent {
for i := range contents {
if contents[i].ObjectMeta.Annotations == nil {
@@ -939,6 +948,16 @@ func newSnapshotArray(
}
}
func withSnapshotInvalidLabel(snapshots []*crdv1.VolumeSnapshot) []*crdv1.VolumeSnapshot {
for i := range snapshots {
if snapshots[i].ObjectMeta.Labels == nil {
snapshots[i].ObjectMeta.Labels = make(map[string]string)
}
snapshots[i].ObjectMeta.Labels[utils.VolumeSnapshotInvalidLabel] = ""
}
return snapshots
}
func newSnapshotClass(snapshotClassName, snapshotClassUID, driverName string, isDefaultClass bool) *crdv1.VolumeSnapshotClass {
sc := &crdv1.VolumeSnapshotClass{
ObjectMeta: metav1.ObjectMeta{

View File

@@ -84,6 +84,17 @@ const controllerUpdateFailMsg = "snapshot controller failed to update"
func (ctrl *csiSnapshotCommonController) syncContent(content *crdv1.VolumeSnapshotContent) error {
snapshotName := utils.SnapshotRefKey(&content.Spec.VolumeSnapshotRef)
klog.V(4).Infof("synchronizing VolumeSnapshotContent[%s]: content is bound to snapshot %s", content.Name, snapshotName)
klog.V(5).Infof("syncContent[%s]: check if we should add invalid label on content", content.Name)
// Perform additional validation. Label objects which fail.
// Part of a plan to tighten validation, this label will enable users to
// query for invalid content objects. See issue #363
content, err := ctrl.checkAndSetInvalidContentLabel(content)
if err != nil {
klog.Errorf("syncContent[%s]: check and add invalid content label failed, %s", content.Name, err.Error())
return err
}
// TODO(xiangqian): Putting this check in controller as webhook has not been implemented
// yet. Remove the source checking once issue #187 is resolved:
// https://github.com/kubernetes-csi/external-snapshotter/issues/187
@@ -114,7 +125,7 @@ func (ctrl *csiSnapshotCommonController) syncContent(content *crdv1.VolumeSnapsh
// and it may have already been deleted, and it will fall into the
// snapshot == nil case below
var snapshot *crdv1.VolumeSnapshot
snapshot, err := ctrl.getSnapshotFromStore(snapshotName)
snapshot, err = ctrl.getSnapshotFromStore(snapshotName)
if err != nil {
return err
}
@@ -167,6 +178,7 @@ func (ctrl *csiSnapshotCommonController) syncContent(content *crdv1.VolumeSnapsh
// For easier readability, it is split into syncUnreadySnapshot and syncReadySnapshot
func (ctrl *csiSnapshotCommonController) syncSnapshot(snapshot *crdv1.VolumeSnapshot) error {
klog.V(5).Infof("synchronizing VolumeSnapshot[%s]: %s", utils.SnapshotKey(snapshot), utils.GetSnapshotStatusForLogging(snapshot))
klog.V(5).Infof("syncSnapshot [%s]: check if we should remove finalizer on snapshot PVC source and remove it if we can", utils.SnapshotKey(snapshot))
// Check if we should remove finalizer on PVC and remove it if we can.
@@ -176,6 +188,16 @@ func (ctrl *csiSnapshotCommonController) syncSnapshot(snapshot *crdv1.VolumeSnap
ctrl.eventRecorder.Event(snapshot, v1.EventTypeWarning, "ErrorPVCFinalizer", "Error check and remove PVC Finalizer for VolumeSnapshot")
}
klog.V(5).Infof("syncSnapshot[%s]: check if we should add invalid label on snapshot", utils.SnapshotKey(snapshot))
// Perform additional validation. Label objects which fail.
// Part of a plan to tighten validation, this label will enable users to
// query for invalid snapshot objects. See issue #363
snapshot, err := ctrl.checkAndSetInvalidSnapshotLabel(snapshot)
if err != nil {
klog.Errorf("syncSnapshot[%s]: check and add invalid snapshot label failed, %s", utils.SnapshotKey(snapshot), err.Error())
return err
}
// Proceed with snapshot deletion only if snapshot is not in the middled of being
// created from a PVC with a finalizer. This is to ensure that the PVC finalizer
// can be removed even if a delete snapshot request is received before create
@@ -1285,7 +1307,7 @@ func (ctrl *csiSnapshotCommonController) addSnapshotFinalizer(snapshot *crdv1.Vo
}
_, err := ctrl.clientset.SnapshotV1beta1().VolumeSnapshots(snapshotClone.Namespace).Update(context.TODO(), snapshotClone, metav1.UpdateOptions{})
if err != nil {
return newControllerUpdateError(snapshot.Name, err.Error())
return newControllerUpdateError(utils.SnapshotKey(snapshot), err.Error())
}
_, err = ctrl.storeSnapshotUpdate(snapshotClone)
@@ -1374,3 +1396,87 @@ func (ctrl *csiSnapshotCommonController) setAnnVolumeSnapshotBeingDeleted(conten
}
return nil
}
// checkAndSetInvalidContentLabel adds a label to unlabeled invalid content objects and removes the label from valid ones.
func (ctrl *csiSnapshotCommonController) checkAndSetInvalidContentLabel(content *crdv1.VolumeSnapshotContent) (*crdv1.VolumeSnapshotContent, error) {
hasLabel := utils.MapContainsKey(content.ObjectMeta.Labels, utils.VolumeSnapshotContentInvalidLabel)
err := utils.ValidateSnapshotContent(content)
if err != nil {
klog.Errorf("syncContent[%s]: Invalid content detected, %s", content.Name, err.Error())
}
// If the snapshot content correctly has the label, or correctly does not have the label, take no action.
if hasLabel && err != nil || !hasLabel && err == nil {
return content, nil
}
contentClone := content.DeepCopy()
if hasLabel {
// Need to remove the label
delete(contentClone.Labels, utils.VolumeSnapshotContentInvalidLabel)
} else {
// Snapshot content is invalid and does not have the label. Need to add the label
if contentClone.ObjectMeta.Labels == nil {
contentClone.ObjectMeta.Labels = make(map[string]string)
}
contentClone.ObjectMeta.Labels[utils.VolumeSnapshotContentInvalidLabel] = ""
}
updatedContent, err := ctrl.clientset.SnapshotV1beta1().VolumeSnapshotContents().Update(context.TODO(), contentClone, metav1.UpdateOptions{})
if err != nil {
return content, newControllerUpdateError(content.Name, err.Error())
}
_, err = ctrl.storeContentUpdate(contentClone)
if err != nil {
klog.Errorf("failed to update content store %v", err)
}
if hasLabel {
klog.V(5).Infof("Removed invalid content label from volume snapshot content %s", content.Name)
} else {
klog.V(5).Infof("Added invalid content label to volume snapshot content %s", content.Name)
}
return updatedContent, nil
}
// checkAndSetInvalidSnapshotLabel adds a label to unlabeled invalid snapshot objects and removes the label from valid ones.
func (ctrl *csiSnapshotCommonController) checkAndSetInvalidSnapshotLabel(snapshot *crdv1.VolumeSnapshot) (*crdv1.VolumeSnapshot, error) {
hasLabel := utils.MapContainsKey(snapshot.ObjectMeta.Labels, utils.VolumeSnapshotInvalidLabel)
err := utils.ValidateSnapshot(snapshot)
if err != nil {
klog.Errorf("syncSnapshot[%s]: Invalid snapshot detected, %s", utils.SnapshotKey(snapshot), err.Error())
}
// If the snapshot correctly has the label, or correctly does not have the label, take no action.
if hasLabel && err != nil || !hasLabel && err == nil {
return snapshot, nil
}
snapshotClone := snapshot.DeepCopy()
if hasLabel {
// Need to remove the label
delete(snapshotClone.Labels, utils.VolumeSnapshotInvalidLabel)
} else {
// Snapshot is invalid and does not have the label. Need to add the label
if snapshotClone.ObjectMeta.Labels == nil {
snapshotClone.ObjectMeta.Labels = make(map[string]string)
}
snapshotClone.ObjectMeta.Labels[utils.VolumeSnapshotInvalidLabel] = ""
}
updatedSnapshot, err := ctrl.clientset.SnapshotV1beta1().VolumeSnapshots(snapshot.Namespace).Update(context.TODO(), snapshotClone, metav1.UpdateOptions{})
if err != nil {
return snapshot, newControllerUpdateError(utils.SnapshotKey(snapshot), err.Error())
}
_, err = ctrl.storeSnapshotUpdate(snapshotClone)
if err != nil {
klog.Errorf("failed to update snapshot store %v", err)
}
if hasLabel {
klog.V(5).Infof("Removed invalid snapshot label from volume snapshot %s", utils.SnapshotKey(snapshot))
} else {
klog.V(5).Infof("Added invalid snapshot label to volume snapshot %s", utils.SnapshotKey(snapshot))
}
return updatedSnapshot, nil
}

View File

@@ -131,8 +131,8 @@ func TestDeleteSync(t *testing.T) {
tests := []controllerTest{
{
name: "1-1 - noop: content will not be deleted if it is bound to a snapshot correctly, snapshot uid is not specified",
initialContents: newContentArray("content1-1", "", "snap1-1", "sid1-1", validSecretClass, "", "", deletePolicy, nil, nil, true),
expectedContents: newContentArray("content1-1", "", "snap1-1", "sid1-1", validSecretClass, "", "", deletePolicy, nil, nil, true),
initialContents: newContentArray("content1-1", "", "snap1-1", "snaphandle1-1", validSecretClass, "snaphandle1-1", "", deletePolicy, nil, nil, true),
expectedContents: newContentArray("content1-1", "", "snap1-1", "snaphandle1-1", validSecretClass, "snaphandle1-1", "", deletePolicy, nil, nil, true),
initialSnapshots: newSnapshotArray("snap1-1", "snapuid1-1", "claim1-1", "", validSecretClass, "content1-1", &False, nil, nil, nil, false, true, &timeNowMetav1),
expectedSnapshots: newSnapshotArray("snap1-1", "snapuid1-1", "claim1-1", "", validSecretClass, "content1-1", &False, nil, nil, nil, false, true, &timeNowMetav1),
expectedEvents: noevents,
@@ -159,8 +159,8 @@ func TestDeleteSync(t *testing.T) {
},
{
name: "1-3 - will not delete content with retain policy set which is bound to a snapshot incorrectly",
initialContents: newContentArray("content1-3", "snapuid1-3-x", "snap1-3", "sid1-3", validSecretClass, "", "", retainPolicy, nil, nil, true),
expectedContents: newContentArray("content1-3", "snapuid1-3-x", "snap1-3", "sid1-3", validSecretClass, "", "", retainPolicy, nil, nil, true),
initialContents: newContentArray("content1-3", "snapuid1-3-x", "snap1-3", "snaphandle1-3", validSecretClass, "snaphandle1-3", "", retainPolicy, nil, nil, true),
expectedContents: newContentArray("content1-3", "snapuid1-3-x", "snap1-3", "snaphandle1-3", validSecretClass, "snaphandle1-3", "", retainPolicy, nil, nil, true),
initialSnapshots: newSnapshotArray("snap1-3", "snapuid1-3", "claim1-3", "", validSecretClass, "content1-3", &False, nil, nil, nil, false, true, &timeNowMetav1),
expectedSnapshots: newSnapshotArray("snap1-3", "snapuid1-3", "claim1-3", "", validSecretClass, "content1-3", &False, nil, nil, nil, false, true, &timeNowMetav1),
expectedEvents: noevents,

View File

@@ -413,7 +413,7 @@ func TestSync(t *testing.T) {
initialContents: nocontents,
expectedContents: nocontents,
initialSnapshots: newSnapshotArray("snap7-2", "snapuid7-2", "", "", validSecretClass, "", &False, nil, nil, nil, false, true, nil),
expectedSnapshots: newSnapshotArray("snap7-2", "snapuid7-2", "", "", validSecretClass, "", &False, nil, nil, newVolumeError("Exactly one of PersistentVolumeClaimName and VolumeSnapshotContentName should be specified"), false, true, nil),
expectedSnapshots: withSnapshotInvalidLabel(newSnapshotArray("snap7-2", "snapuid7-2", "", "", validSecretClass, "", &False, nil, nil, newVolumeError("Exactly one of PersistentVolumeClaimName and VolumeSnapshotContentName should be specified"), false, true, nil)),
expectedEvents: []string{"Warning SnapshotValidationError"},
errors: noerrors,
expectSuccess: false,
@@ -424,7 +424,7 @@ func TestSync(t *testing.T) {
initialContents: nocontents,
expectedContents: nocontents,
initialSnapshots: newSnapshotArray("snap7-3", "snapuid7-3", "claim7-3", "snaphandle7-3", validSecretClass, "", &False, nil, nil, nil, false, true, nil),
expectedSnapshots: newSnapshotArray("snap7-3", "snapuid7-3", "claim7-3", "snaphandle7-3", validSecretClass, "", &False, nil, nil, newVolumeError("Exactly one of PersistentVolumeClaimName and VolumeSnapshotContentName should be specified"), false, true, nil),
expectedSnapshots: withSnapshotInvalidLabel(newSnapshotArray("snap7-3", "snapuid7-3", "claim7-3", "snaphandle7-3", validSecretClass, "", &False, nil, nil, newVolumeError("Exactly one of PersistentVolumeClaimName and VolumeSnapshotContentName should be specified"), false, true, nil)),
initialClaims: newClaimArray("claim7-3", "pvc-uid7-3", "1Gi", "volume7-3", v1.ClaimBound, &classEmpty),
initialVolumes: newVolumeArray("volume7-3", "pv-uid7-3", "pv-handle7-3", "1Gi", "pvc-uid7-3", "claim7-3", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classEmpty),
expectedEvents: []string{"Warning SnapshotValidationError"},
@@ -437,7 +437,7 @@ func TestSync(t *testing.T) {
initialSnapshots: nosnapshots,
expectedSnapshots: nosnapshots,
initialContents: newContentArray("content7-4", "snapuid7-4", "snap7-4", "sid7-4", validSecretClass, "sid7-4", "pv-handle7-4", deletionPolicy, nil, nil, true),
expectedContents: newContentArray("content7-4", "snapuid7-4", "snap7-4", "sid7-4", validSecretClass, "sid7-4", "pv-handle7-4", deletionPolicy, nil, nil, true),
expectedContents: withSnapshotContentInvalidLabel(newContentArray("content7-4", "snapuid7-4", "snap7-4", "sid7-4", validSecretClass, "sid7-4", "pv-handle7-4", deletionPolicy, nil, nil, true)),
expectedEvents: []string{"Warning ContentValidationError"},
errors: noerrors,
expectSuccess: false,
@@ -448,7 +448,7 @@ func TestSync(t *testing.T) {
initialSnapshots: nosnapshots,
expectedSnapshots: nosnapshots,
initialContents: newContentArray("content7-4", "snapuid7-4", "snap7-4", "sid7-4", validSecretClass, "", "", deletionPolicy, nil, nil, true),
expectedContents: newContentArray("content7-4", "snapuid7-4", "snap7-4", "sid7-4", validSecretClass, "", "", deletionPolicy, nil, nil, true),
expectedContents: withSnapshotContentInvalidLabel(newContentArray("content7-4", "snapuid7-4", "snap7-4", "sid7-4", validSecretClass, "", "", deletionPolicy, nil, nil, true)),
expectedEvents: []string{"Warning ContentValidationError"},
errors: noerrors,
expectSuccess: false,

View File

@@ -94,6 +94,13 @@ const (
// and used at snapshot content deletion time.
AnnDeletionSecretRefName = "snapshot.storage.kubernetes.io/deletion-secret-name"
AnnDeletionSecretRefNamespace = "snapshot.storage.kubernetes.io/deletion-secret-namespace"
// VolumeSnapshotContentInvalidLabel is applied to invalid content as a label key. The value does not matter.
// See https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/177-volume-snapshot/tighten-validation-webhook-crd.md#automatic-labelling-of-invalid-objects
VolumeSnapshotContentInvalidLabel = "snapshot.storage.kubernetes.io/invalid-snapshot-content-resource"
// VolumeSnapshotInvalidLabel is applied to invalid snapshot as a label key. The value does not matter.
// See https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/177-volume-snapshot/tighten-validation-webhook-crd.md#automatic-labelling-of-invalid-objects
VolumeSnapshotInvalidLabel = "snapshot.storage.kubernetes.io/invalid-snapshot-resource"
)
var SnapshotterSecretParams = secretParamsMap{
@@ -108,6 +115,61 @@ var SnapshotterListSecretParams = secretParamsMap{
secretNamespaceKey: PrefixedSnapshotterListSecretNamespaceKey,
}
// ValidateSnapshot performs additional strict validation.
// Do NOT rely on this function to fully validate snapshot objects.
// This function will only check the additional rules provided by the webhook.
func ValidateSnapshot(snapshot *crdv1.VolumeSnapshot) error {
if snapshot == nil {
return fmt.Errorf("VolumeSnapshot is nil")
}
source := snapshot.Spec.Source
if source.PersistentVolumeClaimName != nil && source.VolumeSnapshotContentName != nil {
return fmt.Errorf("only one of Spec.Source.PersistentVolumeClaimName = %s and Spec.Source.VolumeSnapshotContentName = %s should be set", *source.PersistentVolumeClaimName, *source.VolumeSnapshotContentName)
}
if source.PersistentVolumeClaimName == nil && source.VolumeSnapshotContentName == nil {
return fmt.Errorf("one of Spec.Source.PersistentVolumeClaimName and Spec.Source.VolumeSnapshotContentName should be set")
}
vscname := snapshot.Spec.VolumeSnapshotClassName
if vscname != nil && *vscname == "" {
return fmt.Errorf("Spec.VolumeSnapshotClassName must not be the empty string")
}
return nil
}
// ValidateSnapshotContent performs additional strict validation.
// Do NOT rely on this function to fully validate snapshot content objects.
// This function will only check the additional rules provided by the webhook.
func ValidateSnapshotContent(snapcontent *crdv1.VolumeSnapshotContent) error {
if snapcontent == nil {
return fmt.Errorf("VolumeSnapshotContent is nil")
}
source := snapcontent.Spec.Source
if source.VolumeHandle != nil && source.SnapshotHandle != nil {
return fmt.Errorf("only one of Spec.Source.VolumeHandle = %s and Spec.Source.SnapshotHandle = %s should be set", *source.VolumeHandle, *source.SnapshotHandle)
}
if source.VolumeHandle == nil && source.SnapshotHandle == nil {
return fmt.Errorf("one of Spec.Source.VolumeHandle and Spec.Source.SnapshotHandle should be set")
}
vsref := snapcontent.Spec.VolumeSnapshotRef
if vsref.Name == "" || vsref.Namespace == "" {
return fmt.Errorf("both Spec.VolumeSnapshotRef.Name = %s and Spec.VolumeSnapshotRef.Namespace = %s must be set", vsref.Name, vsref.Namespace)
}
return nil
}
// MapContainsKey checks if a given map of string to string contains the provided string.
func MapContainsKey(m map[string]string, s string) bool {
_, r := m[s]
return r
}
// ContainsString checks if a given slice of strings contains the provided string.
func ContainsString(slice []string, s string) bool {
for _, item := range slice {

View File

@@ -0,0 +1,41 @@
/*
Copyright 2020 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 (
"crypto/tls"
"k8s.io/klog"
)
// Config contains the server (the webhook) cert and key.
type Config struct {
CertFile string
KeyFile string
}
func configTLS(config Config) *tls.Config {
sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
if err != nil {
klog.Fatal(err)
}
return &tls.Config{
Certificates: []tls.Certificate{sCert},
// TODO: uses mutual tls after we agree on what cert the apiserver should use.
// ClientAuth: tls.RequireAndVerifyClientCert,
}
}

View File

@@ -0,0 +1,105 @@
/*
Copyright 2020 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 (
v1 "k8s.io/api/admission/v1"
"k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func convertAdmissionRequestToV1(r *v1beta1.AdmissionRequest) *v1.AdmissionRequest {
return &v1.AdmissionRequest{
Kind: r.Kind,
Namespace: r.Namespace,
Name: r.Name,
Object: r.Object,
Resource: r.Resource,
Operation: v1.Operation(r.Operation),
UID: r.UID,
DryRun: r.DryRun,
OldObject: r.OldObject,
Options: r.Options,
RequestKind: r.RequestKind,
RequestResource: r.RequestResource,
RequestSubResource: r.RequestSubResource,
SubResource: r.SubResource,
UserInfo: r.UserInfo,
}
}
func convertAdmissionRequestToV1beta1(r *v1.AdmissionRequest) *v1beta1.AdmissionRequest {
return &v1beta1.AdmissionRequest{
Kind: r.Kind,
Namespace: r.Namespace,
Name: r.Name,
Object: r.Object,
Resource: r.Resource,
Operation: v1beta1.Operation(r.Operation),
UID: r.UID,
DryRun: r.DryRun,
OldObject: r.OldObject,
Options: r.Options,
RequestKind: r.RequestKind,
RequestResource: r.RequestResource,
RequestSubResource: r.RequestSubResource,
SubResource: r.SubResource,
UserInfo: r.UserInfo,
}
}
func convertAdmissionResponseToV1(r *v1beta1.AdmissionResponse) *v1.AdmissionResponse {
var pt *v1.PatchType
if r.PatchType != nil {
t := v1.PatchType(*r.PatchType)
pt = &t
}
return &v1.AdmissionResponse{
UID: r.UID,
Allowed: r.Allowed,
AuditAnnotations: r.AuditAnnotations,
Patch: r.Patch,
PatchType: pt,
Result: r.Result,
Warnings: r.Warnings,
}
}
func convertAdmissionResponseToV1beta1(r *v1.AdmissionResponse) *v1beta1.AdmissionResponse {
var pt *v1beta1.PatchType
if r.PatchType != nil {
t := v1beta1.PatchType(*r.PatchType)
pt = &t
}
return &v1beta1.AdmissionResponse{
UID: r.UID,
Allowed: r.Allowed,
AuditAnnotations: r.AuditAnnotations,
Patch: r.Patch,
PatchType: pt,
Result: r.Result,
Warnings: r.Warnings,
}
}
func toV1AdmissionResponse(err error) *v1.AdmissionResponse {
return &v1.AdmissionResponse{
Result: &metav1.Status{
Message: err.Error(),
},
}
}

View File

@@ -0,0 +1,64 @@
/*
Copyright 2020 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"
"math/rand"
"reflect"
"testing"
fuzz "github.com/google/gofuzz"
v1 "k8s.io/api/admission/v1"
"k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/diff"
admissionfuzzer "k8s.io/kubernetes/pkg/apis/admission/fuzzer"
)
func TestConvertAdmissionRequestToV1(t *testing.T) {
f := fuzzer.FuzzerFor(admissionfuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme()))
for i := 0; i < 100; i++ {
t.Run(fmt.Sprintf("Run %d/100", i), func(t *testing.T) {
orig := &v1beta1.AdmissionRequest{}
f.Fuzz(orig)
converted := convertAdmissionRequestToV1(orig)
rt := convertAdmissionRequestToV1beta1(converted)
if !reflect.DeepEqual(orig, rt) {
t.Errorf("expected all request fields to be in converted object but found unaccounted for differences, diff:\n%s", diff.ObjectReflectDiff(orig, converted))
}
})
}
}
func TestConvertAdmissionResponseToV1beta1(t *testing.T) {
f := fuzz.New()
for i := 0; i < 100; i++ {
t.Run(fmt.Sprintf("Run %d/100", i), func(t *testing.T) {
orig := &v1.AdmissionResponse{}
f.Fuzz(orig)
converted := convertAdmissionResponseToV1beta1(orig)
rt := convertAdmissionResponseToV1(converted)
if !reflect.DeepEqual(orig, rt) {
t.Errorf("expected all fields to be in converted object but found unaccounted for differences, diff:\n%s", diff.ObjectReflectDiff(orig, converted))
}
})
}
}

View File

@@ -0,0 +1,43 @@
/*
Copyright 2020 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 (
admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
func init() {
addToScheme(scheme)
}
func addToScheme(scheme *runtime.Scheme) {
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(admissionv1beta1.AddToScheme(scheme))
utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme))
utilruntime.Must(admissionv1.AddToScheme(scheme))
utilruntime.Must(admissionregistrationv1.AddToScheme(scheme))
}

View File

@@ -0,0 +1,197 @@
/*
Copyright 2020 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"
volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v2/apis/volumesnapshot/v1beta1"
"github.com/kubernetes-csi/external-snapshotter/v2/pkg/utils"
v1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
)
var (
// SnapshotV1Beta1GVR is GroupVersionResource for volumesnapshots
SnapshotV1Beta1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1beta1.GroupName, Version: "v1beta1", Resource: "volumesnapshots"}
// SnapshotContentV1Beta1GVR is GroupVersionResource for volumesnapshotcontents
SnapshotContentV1Beta1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1beta1.GroupName, Version: "v1beta1", Resource: "volumesnapshotcontents"}
)
// Add a label {"added-label": "yes"} to the object
func admitSnapshot(ar v1.AdmissionReview) *v1.AdmissionResponse {
klog.V(2).Info("admitting volumesnapshots or volumesnapshotcontents")
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 SnapshotV1Beta1GVR:
snapshot := &volumesnapshotv1beta1.VolumeSnapshot{}
if _, _, err := deserializer.Decode(raw, nil, snapshot); err != nil {
klog.Error(err)
return toV1AdmissionResponse(err)
}
oldSnapshot := &volumesnapshotv1beta1.VolumeSnapshot{}
if _, _, err := deserializer.Decode(oldRaw, nil, oldSnapshot); err != nil {
klog.Error(err)
return toV1AdmissionResponse(err)
}
return decideSnapshot(snapshot, oldSnapshot, isUpdate)
case SnapshotContentV1Beta1GVR:
snapcontent := &volumesnapshotv1beta1.VolumeSnapshotContent{}
if _, _, err := deserializer.Decode(raw, nil, snapcontent); err != nil {
klog.Error(err)
return toV1AdmissionResponse(err)
}
oldSnapcontent := &volumesnapshotv1beta1.VolumeSnapshotContent{}
if _, _, err := deserializer.Decode(oldRaw, nil, oldSnapcontent); err != nil {
klog.Error(err)
return toV1AdmissionResponse(err)
}
return decideSnapshotContent(snapcontent, oldSnapcontent, isUpdate)
default:
err := fmt.Errorf("expect resource to be %s or %s", SnapshotV1Beta1GVR, SnapshotContentV1Beta1GVR)
klog.Error(err)
return toV1AdmissionResponse(err)
}
}
func decideSnapshot(snapshot, oldSnapshot *volumesnapshotv1beta1.VolumeSnapshot, isUpdate bool) *v1.AdmissionResponse {
reviewResponse := &v1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{},
}
if isUpdate {
// if it is an UPDATE and oldSnapshot is not valid, then don't enforce strict validation
// This allows no-op updates to occur on snapshot resources which fail strict validation
// Which allows the remover of finalizers and therefore deletion of this object
// Don't rely on the pointers to be nil, because the deserialization method will convert it to
// The empty struct value. Instead check the operation type.
if err := utils.ValidateSnapshot(oldSnapshot); err != nil {
return reviewResponse
}
// if it is an UPDATE and oldSnapshot is valid, check immutable fields
if err := checkSnapshotImmutableFields(snapshot, oldSnapshot); 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 := utils.ValidateSnapshot(snapshot); err != nil {
reviewResponse.Allowed = false
reviewResponse.Result.Message = err.Error()
}
return reviewResponse
}
func decideSnapshotContent(snapcontent, oldSnapcontent *volumesnapshotv1beta1.VolumeSnapshotContent, isUpdate bool) *v1.AdmissionResponse {
reviewResponse := &v1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{},
}
if isUpdate {
// if it is an UPDATE and oldSnapcontent is not valid, then don't enforce strict validation
// This allows no-op updates to occur on snapshot resources which fail strict validation
// Which allows the remover of finalizers and therefore deletion of this object
// Don't rely on the pointers to be nil, because the deserialization method will convert it to
// The empty struct value. Instead check the operation type.
if err := utils.ValidateSnapshotContent(oldSnapcontent); err != nil {
return reviewResponse
}
// if it is an UPDATE and oldSnapcontent is valid, check immutable fields
if err := checkSnapshotContentImmutableFields(snapcontent, 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 := utils.ValidateSnapshotContent(snapcontent); err != nil {
reviewResponse.Allowed = false
reviewResponse.Result.Message = err.Error()
}
return reviewResponse
}
func strPtrDereference(s *string) string {
if s == nil {
return "<nil string pointer>"
}
return *s
}
func checkSnapshotImmutableFields(snapshot, oldSnapshot *volumesnapshotv1beta1.VolumeSnapshot) error {
if snapshot == nil {
return fmt.Errorf("VolumeSnapshot is nil")
}
if oldSnapshot == nil {
return fmt.Errorf("old VolumeSnapshot is nil")
}
source := snapshot.Spec.Source
oldSource := oldSnapshot.Spec.Source
if !reflect.DeepEqual(source.PersistentVolumeClaimName, oldSource.PersistentVolumeClaimName) {
return fmt.Errorf("Spec.Source.PersistentVolumeClaimName is immutable but was changed from %s to %s", strPtrDereference(oldSource.PersistentVolumeClaimName), strPtrDereference(source.PersistentVolumeClaimName))
}
if !reflect.DeepEqual(source.VolumeSnapshotContentName, oldSource.VolumeSnapshotContentName) {
return fmt.Errorf("Spec.Source.VolumeSnapshotContentName is immutable but was changed from %s to %s", strPtrDereference(oldSource.VolumeSnapshotContentName), strPtrDereference(source.VolumeSnapshotContentName))
}
return nil
}
func checkSnapshotContentImmutableFields(snapcontent, oldSnapcontent *volumesnapshotv1beta1.VolumeSnapshotContent) error {
if snapcontent == nil {
return fmt.Errorf("VolumeSnapshotContent is nil")
}
if oldSnapcontent == nil {
return fmt.Errorf("old VolumeSnapshotContent is nil")
}
source := snapcontent.Spec.Source
oldSource := oldSnapcontent.Spec.Source
if !reflect.DeepEqual(source.VolumeHandle, oldSource.VolumeHandle) {
return fmt.Errorf("Spec.Source.VolumeHandle is immutable but was changed from %s to %s", strPtrDereference(oldSource.VolumeHandle), strPtrDereference(source.VolumeHandle))
}
if !reflect.DeepEqual(source.SnapshotHandle, oldSource.SnapshotHandle) {
return fmt.Errorf("Spec.Source.SnapshotHandle is immutable but was changed from %s to %s", strPtrDereference(oldSource.SnapshotHandle), strPtrDereference(source.SnapshotHandle))
}
return nil
}

View File

@@ -0,0 +1,374 @@
/*
Copyright 2020 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"
volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v2/apis/volumesnapshot/v1beta1"
v1 "k8s.io/api/admission/v1"
core_v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func TestAdmitVolumeSnapshot(t *testing.T) {
pvcname := "pvcname1"
mutatedField := "changed-immutable-field"
contentname := "snapcontent1"
volumeSnapshotClassName := "volume-snapshot-class-1"
emptyVolumeSnapshotClassName := ""
invalidErrorMsg := fmt.Sprintf("only one of Spec.Source.PersistentVolumeClaimName = %s and Spec.Source.VolumeSnapshotContentName = %s should be set", pvcname, contentname)
testCases := []struct {
name string
volumeSnapshot *volumesnapshotv1beta1.VolumeSnapshot
oldVolumeSnapshot *volumesnapshotv1beta1.VolumeSnapshot
shouldAdmit bool
msg string
operation v1.Operation
}{
{
name: "Delete: new and old are nil. Should admit",
volumeSnapshot: nil,
oldVolumeSnapshot: nil,
shouldAdmit: true,
operation: v1.Delete,
},
{
name: "Create: old is nil and new is invalid",
volumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
PersistentVolumeClaimName: &pvcname,
VolumeSnapshotContentName: &contentname,
},
},
},
oldVolumeSnapshot: nil,
shouldAdmit: false,
operation: v1.Create,
msg: invalidErrorMsg,
},
{
name: "Create: old is nil and new is valid",
volumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &contentname,
},
},
},
oldVolumeSnapshot: nil,
shouldAdmit: true,
operation: v1.Create,
},
{
name: "Update: old is valid and new is invalid",
volumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &contentname,
},
VolumeSnapshotClassName: &emptyVolumeSnapshotClassName,
},
},
oldVolumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &contentname,
},
},
},
shouldAdmit: false,
operation: v1.Update,
msg: "Spec.VolumeSnapshotClassName must not be the empty string",
},
{
name: "Update: old is valid and new is valid",
volumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &contentname,
},
VolumeSnapshotClassName: &volumeSnapshotClassName,
},
},
oldVolumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &contentname,
},
},
},
shouldAdmit: true,
operation: v1.Update,
},
{
name: "Update: old is valid and new is valid but changes immutable field spec.source",
volumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &mutatedField,
},
VolumeSnapshotClassName: &volumeSnapshotClassName,
},
},
oldVolumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &contentname,
},
},
},
shouldAdmit: false,
operation: v1.Update,
msg: fmt.Sprintf("Spec.Source.VolumeSnapshotContentName is immutable but was changed from %s to %s", contentname, mutatedField),
},
{
name: "Update: old is invalid and new is valid",
volumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &contentname,
},
},
},
oldVolumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
PersistentVolumeClaimName: &pvcname,
VolumeSnapshotContentName: &contentname,
},
},
},
shouldAdmit: true,
operation: v1.Update,
},
{
name: "Update: old is invalid and new is invalid",
volumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
VolumeSnapshotContentName: &contentname,
PersistentVolumeClaimName: &pvcname,
},
},
},
oldVolumeSnapshot: &volumesnapshotv1beta1.VolumeSnapshot{
Spec: volumesnapshotv1beta1.VolumeSnapshotSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotSource{
PersistentVolumeClaimName: &pvcname,
VolumeSnapshotContentName: &contentname,
},
},
},
shouldAdmit: true,
operation: v1.Update,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
snapshot := tc.volumeSnapshot
raw, err := json.Marshal(snapshot)
if err != nil {
t.Fatal(err)
}
oldSnapshot := tc.oldVolumeSnapshot
oldRaw, err := json.Marshal(oldSnapshot)
if err != nil {
t.Fatal(err)
}
review := v1.AdmissionReview{
Request: &v1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: raw,
},
OldObject: runtime.RawExtension{
Raw: oldRaw,
},
Resource: SnapshotV1Beta1GVR,
Operation: tc.operation,
},
}
response := admitSnapshot(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 TestAdmitVolumeSnapshotContent(t *testing.T) {
volumeHandle := "volumeHandle1"
modifiedField := "modified-field"
snapshotHandle := "snapshotHandle1"
volumeSnapshotClassName := "volume-snapshot-class-1"
validContent := &volumesnapshotv1beta1.VolumeSnapshotContent{
Spec: volumesnapshotv1beta1.VolumeSnapshotContentSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotContentSource{
SnapshotHandle: &snapshotHandle,
},
VolumeSnapshotRef: core_v1.ObjectReference{
Name: "snapshot-ref",
Namespace: "default-ns",
},
VolumeSnapshotClassName: &volumeSnapshotClassName,
},
}
invalidContent := &volumesnapshotv1beta1.VolumeSnapshotContent{
Spec: volumesnapshotv1beta1.VolumeSnapshotContentSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotContentSource{
SnapshotHandle: &snapshotHandle,
VolumeHandle: &volumeHandle,
},
VolumeSnapshotRef: core_v1.ObjectReference{
Name: "",
Namespace: "default-ns",
},
},
}
invalidErrorMsg := fmt.Sprintf("only one of Spec.Source.VolumeHandle = %s and Spec.Source.SnapshotHandle = %s should be set", volumeHandle, snapshotHandle)
testCases := []struct {
name string
volumeSnapshotContent *volumesnapshotv1beta1.VolumeSnapshotContent
oldVolumeSnapshotContent *volumesnapshotv1beta1.VolumeSnapshotContent
shouldAdmit bool
msg string
operation v1.Operation
}{
{
name: "Delete: both new and old are nil",
volumeSnapshotContent: nil,
oldVolumeSnapshotContent: nil,
shouldAdmit: true,
operation: v1.Delete,
},
{
name: "Create: old is nil and new is invalid",
volumeSnapshotContent: invalidContent,
oldVolumeSnapshotContent: nil,
shouldAdmit: false,
operation: v1.Create,
msg: invalidErrorMsg,
},
{
name: "Create: old is nil and new is valid",
volumeSnapshotContent: validContent,
oldVolumeSnapshotContent: nil,
shouldAdmit: true,
operation: v1.Create,
},
{
name: "Update: old is valid and new is invalid",
volumeSnapshotContent: invalidContent,
oldVolumeSnapshotContent: validContent,
shouldAdmit: false,
operation: v1.Update,
msg: fmt.Sprintf("Spec.Source.VolumeHandle is immutable but was changed from %s to %s", strPtrDereference(nil), volumeHandle),
},
{
name: "Update: old is valid and new is valid",
volumeSnapshotContent: validContent,
oldVolumeSnapshotContent: validContent,
shouldAdmit: true,
operation: v1.Update,
},
{
name: "Update: old is valid and new is valid but modifies immutable field",
volumeSnapshotContent: &volumesnapshotv1beta1.VolumeSnapshotContent{
Spec: volumesnapshotv1beta1.VolumeSnapshotContentSpec{
Source: volumesnapshotv1beta1.VolumeSnapshotContentSource{
SnapshotHandle: &modifiedField,
},
VolumeSnapshotRef: core_v1.ObjectReference{
Name: "snapshot-ref",
Namespace: "default-ns",
},
},
},
oldVolumeSnapshotContent: validContent,
shouldAdmit: false,
operation: v1.Update,
msg: fmt.Sprintf("Spec.Source.SnapshotHandle is immutable but was changed from %s to %s", snapshotHandle, modifiedField),
},
{
name: "Update: old is invalid and new is valid",
volumeSnapshotContent: validContent,
oldVolumeSnapshotContent: invalidContent,
shouldAdmit: true,
operation: v1.Update,
},
{
name: "Update: old is invalid and new is invalid",
volumeSnapshotContent: invalidContent,
oldVolumeSnapshotContent: invalidContent,
shouldAdmit: true,
operation: v1.Update,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
snapshotContent := tc.volumeSnapshotContent
raw, err := json.Marshal(snapshotContent)
if err != nil {
t.Fatal(err)
}
oldSnapshotContent := tc.oldVolumeSnapshotContent
oldRaw, err := json.Marshal(oldSnapshotContent)
if err != nil {
t.Fatal(err)
}
review := v1.AdmissionReview{
Request: &v1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: raw,
},
OldObject: runtime.RawExtension{
Raw: oldRaw,
},
Resource: SnapshotContentV1Beta1GVR,
Operation: tc.operation,
},
}
response := admitSnapshot(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)
}
})
}
}

View File

@@ -0,0 +1,197 @@
/*
Copyright 2020 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"
"io/ioutil"
"net/http"
"github.com/spf13/cobra"
v1 "k8s.io/api/admission/v1"
"k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog"
// TODO: try this library to see if it generates correct json patch
// https://github.com/mattbaird/jsonpatch
)
var (
certFile string
keyFile string
port int
)
// CmdWebhook is used by agnhost Cobra.
var CmdWebhook = &cobra.Command{
Use: "validation-webhook",
Short: "Starts a HTTP server, useful for testing MutatingAdmissionWebhook and ValidatingAdmissionWebhook",
Long: `Starts a HTTP server, useful for testing MutatingAdmissionWebhook and ValidatingAdmissionWebhook.
After deploying it to Kubernetes cluster, the Administrator needs to create a ValidatingWebhookConfiguration
in the Kubernetes cluster to register remote webhook admission controllers.`,
Args: cobra.MaximumNArgs(0),
Run: main,
}
func init() {
CmdWebhook.Flags().StringVar(&certFile, "tls-cert-file", "",
"File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert).")
CmdWebhook.Flags().StringVar(&keyFile, "tls-private-key-file", "",
"File containing the default x509 private key matching --tls-cert-file.")
CmdWebhook.Flags().IntVar(&port, "port", 443,
"Secure port that the webhook listens on")
CmdWebhook.MarkFlagRequired("tls-cert-file")
CmdWebhook.MarkFlagRequired("tls-private-key-file")
}
// admitv1beta1Func handles a v1beta1 admission
type admitv1beta1Func func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse
// admitv1beta1Func handles a v1 admission
type admitv1Func func(v1.AdmissionReview) *v1.AdmissionResponse
// admitHandler is a handler, for both validators and mutators, that supports multiple admission review versions
type admitHandler struct {
v1beta1 admitv1beta1Func
v1 admitv1Func
}
func newDelegateToV1AdmitHandler(f admitv1Func) admitHandler {
return admitHandler{
v1beta1: delegateV1beta1AdmitToV1(f),
v1: f,
}
}
func delegateV1beta1AdmitToV1(f admitv1Func) admitv1beta1Func {
return func(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
in := v1.AdmissionReview{Request: convertAdmissionRequestToV1(review.Request)}
out := f(in)
return convertAdmissionResponseToV1beta1(out)
}
}
// serve handles the http portion of a request prior to handing to an admit
// function
func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
var body []byte
if r.Body == nil {
msg := "Expected request body to be non-empty"
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
}
data, err := ioutil.ReadAll(r.Body)
if err != nil {
msg := fmt.Sprintf("Request could not be decoded: %v", err)
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
}
body = data
// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
msg := fmt.Sprintf("contentType=%s, expect application/json", contentType)
klog.Errorf(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
klog.V(2).Info(fmt.Sprintf("handling request: %s", body))
deserializer := codecs.UniversalDeserializer()
obj, gvk, err := deserializer.Decode(body, nil, nil)
if err != nil {
msg := fmt.Sprintf("Request could not be decoded: %v", err)
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
var responseObj runtime.Object
switch *gvk {
case v1beta1.SchemeGroupVersion.WithKind("AdmissionReview"):
requestedAdmissionReview, ok := obj.(*v1beta1.AdmissionReview)
if !ok {
msg := fmt.Sprintf("Expected v1beta1.AdmissionReview but got: %T", obj)
klog.Errorf(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
responseAdmissionReview := &v1beta1.AdmissionReview{}
responseAdmissionReview.SetGroupVersionKind(*gvk)
responseAdmissionReview.Response = admit.v1beta1(*requestedAdmissionReview)
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
responseObj = responseAdmissionReview
case v1.SchemeGroupVersion.WithKind("AdmissionReview"):
requestedAdmissionReview, ok := obj.(*v1.AdmissionReview)
if !ok {
msg := fmt.Sprintf("Expected v1.AdmissionReview but got: %T", obj)
klog.Errorf(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
responseAdmissionReview := &v1.AdmissionReview{}
responseAdmissionReview.SetGroupVersionKind(*gvk)
responseAdmissionReview.Response = admit.v1(*requestedAdmissionReview)
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
responseObj = responseAdmissionReview
default:
msg := fmt.Sprintf("Unsupported group version kind: %v", gvk)
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
klog.V(2).Info(fmt.Sprintf("sending response: %v", responseObj))
respBytes, err := json.Marshal(responseObj)
if err != nil {
klog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(respBytes); err != nil {
klog.Error(err)
}
}
func serveSnapshotRequest(w http.ResponseWriter, r *http.Request) {
serve(w, r, newDelegateToV1AdmitHandler(admitSnapshot))
}
func main(cmd *cobra.Command, args []string) {
fmt.Println("Starting webhook server")
config := Config{
CertFile: certFile,
KeyFile: keyFile,
}
http.HandleFunc("/volumesnapshot", serveSnapshotRequest)
http.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) })
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
TLSConfig: configTLS(config),
}
err := server.ListenAndServeTLS("", "")
if err != nil {
panic(err)
}
}