Added validation to prevent mutating Claim.Spec after binding

This commit is contained in:
markturansky
2015-03-30 18:07:53 -04:00
parent 95bd170ca2
commit c4a53a23bd
3 changed files with 150 additions and 9 deletions

View File

@@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"net" "net"
"path" "path"
"reflect"
"strings" "strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
@@ -28,6 +29,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
errs "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" errs "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume"
"github.com/golang/glog" "github.com/golang/glog"
) )
@@ -421,7 +423,7 @@ func ValidatePersistentVolumeClaim(pvc *api.PersistentVolumeClaim) errs.Validati
allErrs = append(allErrs, errs.NewFieldInvalid("persistentVolumeClaim.Spec.AccessModes", pvc.Spec.AccessModes, "at least 1 AccessModeType is required")) allErrs = append(allErrs, errs.NewFieldInvalid("persistentVolumeClaim.Spec.AccessModes", pvc.Spec.AccessModes, "at least 1 AccessModeType is required"))
} }
if len(pvc.Spec.Resources.Requests) == 0 { if len(pvc.Spec.Resources.Requests) == 0 {
allErrs = append(allErrs, errs.NewFieldInvalid("persistentVolumeClaim.Spec.Resources.Requests", pvc.Spec.AccessModes, "No Resource.Requests specified")) allErrs = append(allErrs, errs.NewFieldInvalid("persistentVolumeClaim.Spec.Resources.Requests", pvc.Spec.Resources.Requests, "No Resource.Requests specified"))
} }
return allErrs return allErrs
} }
@@ -429,6 +431,16 @@ func ValidatePersistentVolumeClaim(pvc *api.PersistentVolumeClaim) errs.Validati
func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *api.PersistentVolumeClaim) errs.ValidationErrorList { func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *api.PersistentVolumeClaim) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{} allErrs := errs.ValidationErrorList{}
allErrs = ValidatePersistentVolumeClaim(newPvc) allErrs = ValidatePersistentVolumeClaim(newPvc)
if oldPvc.Status.VolumeRef != nil {
oldModesAsString := volume.GetAccessModesAsString(oldPvc.Spec.AccessModes)
newModesAsString := volume.GetAccessModesAsString(newPvc.Spec.AccessModes)
if oldModesAsString != newModesAsString {
allErrs = append(allErrs, errs.NewFieldInvalid("spec.AccessModes", oldPvc.Spec.AccessModes, "field is immutable"))
}
if !reflect.DeepEqual(oldPvc.Spec.Resources, newPvc.Spec.Resources) {
allErrs = append(allErrs, errs.NewFieldInvalid("spec.Resources", oldPvc.Spec.Resources, "field is immutable"))
}
}
newPvc.Status = oldPvc.Status newPvc.Status = oldPvc.Status
return allErrs return allErrs
} }

View File

@@ -366,6 +366,136 @@ func TestValidatePersistentVolumeClaim(t *testing.T) {
} }
} }
func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
pvcA := &api.PersistentVolumeClaim{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: "ns",
Labels: map[string]string{
"nice-label": "fizzbuzz",
},
},
Spec: api.PersistentVolumeClaimSpec{
AccessModes: []api.AccessModeType{
api.ReadWriteOnce,
},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
},
},
},
}
// AccessModes differ from pvcA
pvcB := &api.PersistentVolumeClaim{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: "ns",
Labels: map[string]string{
"nice-label": "fizzbuzz",
},
},
Spec: api.PersistentVolumeClaimSpec{
AccessModes: []api.AccessModeType{
api.ReadWriteMany,
},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
},
},
},
}
// Resources differ from pvcA
pvcC := &api.PersistentVolumeClaim{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: "ns",
Labels: map[string]string{
"nice-label": "fizzbuzz",
},
},
Spec: api.PersistentVolumeClaimSpec{
AccessModes: []api.AccessModeType{
api.ReadWriteOnce,
},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("7G"),
},
},
},
}
// Labels differ from pvcA
pvcD := &api.PersistentVolumeClaim{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: "ns",
Labels: map[string]string{
"nice-label": "buzzfizz",
},
},
Spec: api.PersistentVolumeClaimSpec{
AccessModes: []api.AccessModeType{
api.ReadWriteOnce,
},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
},
},
},
}
scenarios := map[string]struct {
isExpectedFailure bool
oldClaim *api.PersistentVolumeClaim
newClaim *api.PersistentVolumeClaim
}{
"invalid-accessmodes-change": {
isExpectedFailure: true,
oldClaim: pvcA,
newClaim: pvcB,
},
"invalid-resources-change": {
isExpectedFailure: true,
oldClaim: pvcA,
newClaim: pvcC,
},
"valid-label-change": {
isExpectedFailure: false,
oldClaim: pvcA,
newClaim: pvcD,
},
}
// validation errors on Update only occur if the Claim is already bound.
// failures are only expected after binding
for name, scenario := range scenarios {
errs := ValidatePersistentVolumeClaimUpdate(scenario.newClaim, scenario.oldClaim)
if len(errs) > 0 && !scenario.isExpectedFailure {
t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs)
}
}
// 2 of 3 scenarios above are invalid if the old PVC has a bound PV
for name, scenario := range scenarios {
scenario.oldClaim.Status.VolumeRef = &api.ObjectReference{Name: "foo", Namespace: "ns"}
errs := ValidatePersistentVolumeClaimUpdate(scenario.newClaim, scenario.oldClaim)
if len(errs) == 0 && scenario.isExpectedFailure {
t.Errorf("Unexpected success for scenario: %s", name)
}
if len(errs) > 0 && !scenario.isExpectedFailure {
t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs)
}
}
}
func TestValidateVolumes(t *testing.T) { func TestValidateVolumes(t *testing.T) {
successCase := []api.Volume{ successCase := []api.Volume{
{Name: "abc", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path1"}}}, {Name: "abc", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path1"}}},

View File

@@ -24,24 +24,23 @@ func GetAccessModesAsString(modes []api.AccessModeType) string {
modesAsString := "" modesAsString := ""
if contains(modes, api.ReadWriteOnce) { if contains(modes, api.ReadWriteOnce) {
appendAccessMode(modesAsString, "RWO") appendAccessMode(&modesAsString, "RWO")
} }
if contains(modes, api.ReadOnlyMany) { if contains(modes, api.ReadOnlyMany) {
appendAccessMode(modesAsString, "ROX") appendAccessMode(&modesAsString, "ROX")
} }
if contains(modes, api.ReadWriteMany) { if contains(modes, api.ReadWriteMany) {
appendAccessMode(modesAsString, "RWX") appendAccessMode(&modesAsString, "RWX")
} }
return modesAsString return modesAsString
} }
func appendAccessMode(modes, mode string) string { func appendAccessMode(modes *string, mode string) {
if modes != "" { if *modes != "" {
modes += "," *modes += ","
} }
modes += mode *modes += mode
return modes
} }
func contains(modes []api.AccessModeType, mode api.AccessModeType) bool { func contains(modes []api.AccessModeType, mode api.AccessModeType) bool {