From e6df0b1a24c86ec32131427c599a653a22b4bc96 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Wed, 4 Nov 2015 13:52:14 -0800 Subject: [PATCH] Convert validation to use FieldPath Before this change we have a mish-mash of ways to pass field names around for error generation. Sometimes string fieldnames, sometimes .Prefix(), sometimes neither, often wrong names or not indexed when it should be. Instead of that mess, this is part one of a couple of commits that will make it more strongly typed and hopefully encourage correct behavior. At least you will have to think about field names, which is better than nothing. It turned out to be really hard to do this incrementally. --- examples/examples_test.go | 2 +- pkg/api/errors/errors_test.go | 10 +- pkg/api/rest/create.go | 2 +- pkg/api/rest/update.go | 6 +- pkg/api/v1/backward_compatibility_test.go | 2 +- pkg/api/validation/events.go | 6 +- pkg/api/validation/validation.go | 973 +++++++++--------- pkg/api/validation/validation_test.go | 360 +++++-- pkg/apis/extensions/validation/validation.go | 291 +++--- .../extensions/validation/validation_test.go | 41 +- pkg/kubectl/cmd/util/helpers_test.go | 8 +- pkg/kubelet/config/config.go | 3 +- pkg/kubelet/kubelet.go | 3 +- pkg/labels/selector.go | 5 +- pkg/registry/pod/etcd/etcd.go | 4 +- pkg/registry/service/rest.go | 12 +- pkg/storage/util.go | 7 +- pkg/util/validation/errors.go | 32 +- pkg/util/validation/errors_test.go | 26 +- 19 files changed, 986 insertions(+), 807 deletions(-) diff --git a/examples/examples_test.go b/examples/examples_test.go index 491df18bd62..77fdb623050 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -123,7 +123,7 @@ func validateObject(obj runtime.Object) (errors utilvalidation.ErrorList) { } errors = expvalidation.ValidateDaemonSet(t) default: - return utilvalidation.ErrorList{utilvalidation.NewInternalError("", fmt.Errorf("no validation defined for %#v", obj))} + return utilvalidation.ErrorList{utilvalidation.NewInternalError(utilvalidation.NewFieldPath(""), fmt.Errorf("no validation defined for %#v", obj))} } return errors } diff --git a/pkg/api/errors/errors_test.go b/pkg/api/errors/errors_test.go index 182093f6f0e..5c428b3b73a 100644 --- a/pkg/api/errors/errors_test.go +++ b/pkg/api/errors/errors_test.go @@ -92,7 +92,7 @@ func TestNewInvalid(t *testing.T) { Details *unversioned.StatusDetails }{ { - validation.NewDuplicateError("field[0].name", "bar"), + validation.NewDuplicateError(validation.NewFieldPath("field[0].name"), "bar"), &unversioned.StatusDetails{ Kind: "kind", Name: "name", @@ -103,7 +103,7 @@ func TestNewInvalid(t *testing.T) { }, }, { - validation.NewInvalidError("field[0].name", "bar", "detail"), + validation.NewInvalidError(validation.NewFieldPath("field[0].name"), "bar", "detail"), &unversioned.StatusDetails{ Kind: "kind", Name: "name", @@ -114,7 +114,7 @@ func TestNewInvalid(t *testing.T) { }, }, { - validation.NewNotFoundError("field[0].name", "bar"), + validation.NewNotFoundError(validation.NewFieldPath("field[0].name"), "bar"), &unversioned.StatusDetails{ Kind: "kind", Name: "name", @@ -125,7 +125,7 @@ func TestNewInvalid(t *testing.T) { }, }, { - validation.NewNotSupportedError("field[0].name", "bar", nil), + validation.NewNotSupportedError(validation.NewFieldPath("field[0].name"), "bar", nil), &unversioned.StatusDetails{ Kind: "kind", Name: "name", @@ -136,7 +136,7 @@ func TestNewInvalid(t *testing.T) { }, }, { - validation.NewRequiredError("field[0].name"), + validation.NewRequiredError(validation.NewFieldPath("field[0].name")), &unversioned.StatusDetails{ Kind: "kind", Name: "name", diff --git a/pkg/api/rest/create.go b/pkg/api/rest/create.go index 57bd59d73a9..574cdc587f4 100644 --- a/pkg/api/rest/create.go +++ b/pkg/api/rest/create.go @@ -77,7 +77,7 @@ func BeforeCreate(strategy RESTCreateStrategy, ctx api.Context, obj runtime.Obje // Custom validation (including name validation) passed // Now run common validation on object meta // Do this *after* custom validation so that specific error messages are shown whenever possible - if errs := validation.ValidateObjectMeta(objectMeta, strategy.NamespaceScoped(), validation.ValidatePathSegmentName); len(errs) > 0 { + if errs := validation.ValidateObjectMeta(objectMeta, strategy.NamespaceScoped(), validation.ValidatePathSegmentName, utilvalidation.NewFieldPath("metadata")); len(errs) > 0 { return errors.NewInvalid(kind, objectMeta.Name, errs) } diff --git a/pkg/api/rest/update.go b/pkg/api/rest/update.go index c8e9e30d784..fb168e688d0 100644 --- a/pkg/api/rest/update.go +++ b/pkg/api/rest/update.go @@ -57,13 +57,13 @@ func validateCommonFields(obj, old runtime.Object) utilvalidation.ErrorList { allErrs := utilvalidation.ErrorList{} objectMeta, err := api.ObjectMetaFor(obj) if err != nil { - return append(allErrs, utilvalidation.NewInternalError("metadata", err)) + return append(allErrs, utilvalidation.NewInternalError(utilvalidation.NewFieldPath("metadata"), err)) } oldObjectMeta, err := api.ObjectMetaFor(old) if err != nil { - return append(allErrs, utilvalidation.NewInternalError("metadata", err)) + return append(allErrs, utilvalidation.NewInternalError(utilvalidation.NewFieldPath("metadata"), err)) } - allErrs = append(allErrs, validation.ValidateObjectMetaUpdate(objectMeta, oldObjectMeta)...) + allErrs = append(allErrs, validation.ValidateObjectMetaUpdate(objectMeta, oldObjectMeta, utilvalidation.NewFieldPath("metadata"))...) return allErrs } diff --git a/pkg/api/v1/backward_compatibility_test.go b/pkg/api/v1/backward_compatibility_test.go index 1dc468c9160..d919ed2b991 100644 --- a/pkg/api/v1/backward_compatibility_test.go +++ b/pkg/api/v1/backward_compatibility_test.go @@ -218,7 +218,7 @@ func TestCompatibility_v1_PodSecurityContext(t *testing.T) { } validator := func(obj runtime.Object) utilvalidation.ErrorList { - return validation.ValidatePodSpec(&(obj.(*api.Pod).Spec)) + return validation.ValidatePodSpec(&(obj.(*api.Pod).Spec), utilvalidation.NewFieldPath("spec")) } for _, tc := range cases { diff --git a/pkg/api/validation/events.go b/pkg/api/validation/events.go index 880f38cc787..6eba8904726 100644 --- a/pkg/api/validation/events.go +++ b/pkg/api/validation/events.go @@ -27,14 +27,14 @@ func ValidateEvent(event *api.Event) validation.ErrorList { // There is no namespace required for node. if event.InvolvedObject.Kind == "Node" && event.Namespace != "" { - allErrs = append(allErrs, validation.NewInvalidError("involvedObject.namespace", event.InvolvedObject.Namespace, "namespace is not required for node")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("involvedObject", "namespace"), event.InvolvedObject.Namespace, "namespace is not required for node")) } if event.InvolvedObject.Kind != "Node" && event.Namespace != event.InvolvedObject.Namespace { - allErrs = append(allErrs, validation.NewInvalidError("involvedObject.namespace", event.InvolvedObject.Namespace, "namespace does not match involvedObject")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("involvedObject", "namespace"), event.InvolvedObject.Namespace, "does not match involvedObject")) } if !validation.IsDNS1123Subdomain(event.Namespace) { - allErrs = append(allErrs, validation.NewInvalidError("namespace", event.Namespace, "")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("namespace"), event.Namespace, "")) } return allErrs } diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 570879717f3..5fc10f7de05 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -61,38 +61,38 @@ var PortNameErrorMsg string = fmt.Sprintf(`must be an IANA_SVC_NAME (at most 15 const totalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB -func ValidateLabelName(labelName, fieldName string) validation.ErrorList { +func ValidateLabelName(labelName string, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if !validation.IsQualifiedName(labelName) { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, labelName, qualifiedNameErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, labelName, qualifiedNameErrorMsg)) } return allErrs } // ValidateLabels validates that a set of labels are correctly defined. -func ValidateLabels(labels map[string]string, field string) validation.ErrorList { +func ValidateLabels(labels map[string]string, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} for k, v := range labels { - allErrs = append(allErrs, ValidateLabelName(k, field)...) + allErrs = append(allErrs, ValidateLabelName(k, fldPath)...) if !validation.IsValidLabelValue(v) { - allErrs = append(allErrs, validation.NewInvalidError(field, v, labelValueErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, v, labelValueErrorMsg)) } } return allErrs } // ValidateAnnotations validates that a set of annotations are correctly defined. -func ValidateAnnotations(annotations map[string]string, field string) validation.ErrorList { +func ValidateAnnotations(annotations map[string]string, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} var totalSize int64 for k, v := range annotations { if !validation.IsQualifiedName(strings.ToLower(k)) { - allErrs = append(allErrs, validation.NewInvalidError(field, k, qualifiedNameErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, k, qualifiedNameErrorMsg)) } totalSize += (int64)(len(k)) + (int64)(len(v)) } if totalSize > (int64)(totalAnnotationSizeLimitB) { - allErrs = append(allErrs, validation.NewTooLongError(field, "", totalAnnotationSizeLimitB)) + allErrs = append(allErrs, validation.NewTooLongError(fldPath, "", totalAnnotationSizeLimitB)) } return allErrs } @@ -217,27 +217,27 @@ func NameIsDNS952Label(name string, prefix bool) (bool, string) { } // Validates that given value is not negative. -func ValidatePositiveField(value int64, fieldName string) validation.ErrorList { +func ValidatePositiveField(value int64, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if value < 0 { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, value, isNegativeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, value, isNegativeErrorMsg)) } return allErrs } // Validates that a Quantity is not negative -func ValidatePositiveQuantity(value resource.Quantity, fieldName string) validation.ErrorList { +func ValidatePositiveQuantity(value resource.Quantity, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if value.Cmp(resource.Quantity{}) < 0 { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, value.String(), isNegativeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, value.String(), isNegativeErrorMsg)) } return allErrs } -func ValidateImmutableField(new, old interface{}, fieldName string) validation.ErrorList { +func ValidateImmutableField(newVal, oldVal interface{}, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - if !api.Semantic.DeepEqual(old, new) { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, new, fieldImmutableErrorMsg)) + if !api.Semantic.DeepEqual(oldVal, newVal) { + allErrs = append(allErrs, validation.NewInvalidError(fldPath, newVal, fieldImmutableErrorMsg)) } return allErrs } @@ -246,122 +246,124 @@ func ValidateImmutableField(new, old interface{}, fieldName string) validation.E // been performed. // It doesn't return an error for rootscoped resources with namespace, because namespace should already be cleared before. // TODO: Remove calls to this method scattered in validations of specific resources, e.g., ValidatePodUpdate. -func ValidateObjectMeta(meta *api.ObjectMeta, requiresNamespace bool, nameFn ValidateNameFunc) validation.ErrorList { +func ValidateObjectMeta(meta *api.ObjectMeta, requiresNamespace bool, nameFn ValidateNameFunc, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(meta.GenerateName) != 0 { if ok, qualifier := nameFn(meta.GenerateName, true); !ok { - allErrs = append(allErrs, validation.NewInvalidError("generateName", meta.GenerateName, qualifier)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("generateName"), meta.GenerateName, qualifier)) } } // If the generated name validates, but the calculated value does not, it's a problem with generation, and we // report it here. This may confuse users, but indicates a programming bug and still must be validated. // If there are multiple fields out of which one is required then add a or as a separator if len(meta.Name) == 0 { - requiredErr := validation.NewRequiredError("name") + requiredErr := validation.NewRequiredError(fldPath.Child("name")) requiredErr.Detail = "name or generateName is required" allErrs = append(allErrs, requiredErr) } else { if ok, qualifier := nameFn(meta.Name, false); !ok { - allErrs = append(allErrs, validation.NewInvalidError("name", meta.Name, qualifier)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("name"), meta.Name, qualifier)) } } - allErrs = append(allErrs, ValidatePositiveField(meta.Generation, "generation")...) + allErrs = append(allErrs, ValidatePositiveField(meta.Generation, fldPath.Child("generation"))...) if requiresNamespace { if len(meta.Namespace) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("namespace")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("namespace"))) } else if ok, _ := ValidateNamespaceName(meta.Namespace, false); !ok { - allErrs = append(allErrs, validation.NewInvalidError("namespace", meta.Namespace, DNS1123LabelErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("namespace"), meta.Namespace, DNS1123LabelErrorMsg)) } } else { if len(meta.Namespace) != 0 { - allErrs = append(allErrs, validation.NewInvalidError("namespace", meta.Namespace, "namespace is not allowed on this type")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("namespace"), meta.Namespace, "namespace is not allowed on this type")) } } - allErrs = append(allErrs, ValidateLabels(meta.Labels, "labels")...) - allErrs = append(allErrs, ValidateAnnotations(meta.Annotations, "annotations")...) + allErrs = append(allErrs, ValidateLabels(meta.Labels, fldPath.Child("labels"))...) + allErrs = append(allErrs, ValidateAnnotations(meta.Annotations, fldPath.Child("annotations"))...) return allErrs } // ValidateObjectMetaUpdate validates an object's metadata when updated -func ValidateObjectMetaUpdate(new, old *api.ObjectMeta) validation.ErrorList { +func ValidateObjectMetaUpdate(newMeta, oldMeta *api.ObjectMeta, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - if !RepairMalformedUpdates && new.UID != old.UID { - allErrs = append(allErrs, validation.NewInvalidError("uid", new.UID, "field is immutable")) + if !RepairMalformedUpdates && newMeta.UID != oldMeta.UID { + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("uid"), newMeta.UID, "field is immutable")) } // in the event it is left empty, set it, to allow clients more flexibility // TODO: remove the following code that repairs the update request when we retire the clients that modify the immutable fields. // Please do not copy this pattern elsewhere; validation functions should not be modifying the objects they are passed! if RepairMalformedUpdates { - if len(new.UID) == 0 { - new.UID = old.UID + if len(newMeta.UID) == 0 { + newMeta.UID = oldMeta.UID } // ignore changes to timestamp - if old.CreationTimestamp.IsZero() { - old.CreationTimestamp = new.CreationTimestamp + if oldMeta.CreationTimestamp.IsZero() { + oldMeta.CreationTimestamp = newMeta.CreationTimestamp } else { - new.CreationTimestamp = old.CreationTimestamp + newMeta.CreationTimestamp = oldMeta.CreationTimestamp } // an object can never remove a deletion timestamp or clear/change grace period seconds - if !old.DeletionTimestamp.IsZero() { - new.DeletionTimestamp = old.DeletionTimestamp + if !oldMeta.DeletionTimestamp.IsZero() { + newMeta.DeletionTimestamp = oldMeta.DeletionTimestamp } - if old.DeletionGracePeriodSeconds != nil && new.DeletionGracePeriodSeconds == nil { - new.DeletionGracePeriodSeconds = old.DeletionGracePeriodSeconds + if oldMeta.DeletionGracePeriodSeconds != nil && newMeta.DeletionGracePeriodSeconds == nil { + newMeta.DeletionGracePeriodSeconds = oldMeta.DeletionGracePeriodSeconds } } - // TODO: needs to check if new==nil && old !=nil after the repair logic is removed. - if new.DeletionGracePeriodSeconds != nil && old.DeletionGracePeriodSeconds != nil && *new.DeletionGracePeriodSeconds != *old.DeletionGracePeriodSeconds { - allErrs = append(allErrs, validation.NewInvalidError("deletionGracePeriodSeconds", new.DeletionGracePeriodSeconds, "field is immutable; may only be changed via deletion")) + // TODO: needs to check if newMeta==nil && oldMeta !=nil after the repair logic is removed. + if newMeta.DeletionGracePeriodSeconds != nil && oldMeta.DeletionGracePeriodSeconds != nil && *newMeta.DeletionGracePeriodSeconds != *oldMeta.DeletionGracePeriodSeconds { + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("deletionGracePeriodSeconds"), newMeta.DeletionGracePeriodSeconds, "field is immutable; may only be changed via deletion")) } // Reject updates that don't specify a resource version - if new.ResourceVersion == "" { - allErrs = append(allErrs, validation.NewInvalidError("resourceVersion", new.ResourceVersion, "resourceVersion must be specified for an update")) + if newMeta.ResourceVersion == "" { + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("resourceVersion"), newMeta.ResourceVersion, "resourceVersion must be specified for an update")) } - allErrs = append(allErrs, ValidateImmutableField(new.Name, old.Name, "name")...) - allErrs = append(allErrs, ValidateImmutableField(new.Namespace, old.Namespace, "namespace")...) - allErrs = append(allErrs, ValidateImmutableField(new.UID, old.UID, "uid")...) - allErrs = append(allErrs, ValidateImmutableField(new.CreationTimestamp, old.CreationTimestamp, "creationTimestamp")...) + allErrs = append(allErrs, ValidateImmutableField(newMeta.Name, oldMeta.Name, fldPath.Child("name"))...) + allErrs = append(allErrs, ValidateImmutableField(newMeta.Namespace, oldMeta.Namespace, fldPath.Child("namespace"))...) + allErrs = append(allErrs, ValidateImmutableField(newMeta.UID, oldMeta.UID, fldPath.Child("uid"))...) + allErrs = append(allErrs, ValidateImmutableField(newMeta.CreationTimestamp, oldMeta.CreationTimestamp, fldPath.Child("creationTimestamp"))...) - allErrs = append(allErrs, ValidateLabels(new.Labels, "labels")...) - allErrs = append(allErrs, ValidateAnnotations(new.Annotations, "annotations")...) + allErrs = append(allErrs, ValidateLabels(newMeta.Labels, fldPath.Child("labels"))...) + allErrs = append(allErrs, ValidateAnnotations(newMeta.Annotations, fldPath.Child("annotations"))...) return allErrs } -func validateVolumes(volumes []api.Volume) (sets.String, validation.ErrorList) { +func validateVolumes(volumes []api.Volume, fldPath *validation.FieldPath) (sets.String, validation.ErrorList) { allErrs := validation.ErrorList{} allNames := sets.String{} for i, vol := range volumes { - el := validateSource(&vol.VolumeSource).Prefix("source") + idxPath := fldPath.Index(i) + el := validateVolumeSource(&vol.VolumeSource, idxPath) if len(vol.Name) == 0 { - el = append(el, validation.NewRequiredError("name")) + el = append(el, validation.NewRequiredError(idxPath.Child("name"))) } else if !validation.IsDNS1123Label(vol.Name) { - el = append(el, validation.NewInvalidError("name", vol.Name, DNS1123LabelErrorMsg)) + el = append(el, validation.NewInvalidError(idxPath.Child("name"), vol.Name, DNS1123LabelErrorMsg)) } else if allNames.Has(vol.Name) { - el = append(el, validation.NewDuplicateError("name", vol.Name)) + el = append(el, validation.NewDuplicateError(idxPath.Child("name"), vol.Name)) } if len(el) == 0 { allNames.Insert(vol.Name) } else { - allErrs = append(allErrs, el.PrefixIndex(i)...) + allErrs = append(allErrs, el...) } + } return allNames, allErrs } -func validateSource(source *api.VolumeSource) validation.ErrorList { +func validateVolumeSource(source *api.VolumeSource, fldPath *validation.FieldPath) validation.ErrorList { numVolumes := 0 allErrs := validation.ErrorList{} if source.HostPath != nil { numVolumes++ - allErrs = append(allErrs, validateHostPathVolumeSource(source.HostPath).Prefix("hostPath")...) + allErrs = append(allErrs, validateHostPathVolumeSource(source.HostPath, fldPath.Child("hostPath"))...) } if source.EmptyDir != nil { numVolumes++ @@ -369,212 +371,212 @@ func validateSource(source *api.VolumeSource) validation.ErrorList { } if source.GitRepo != nil { numVolumes++ - allErrs = append(allErrs, validateGitRepoVolumeSource(source.GitRepo).Prefix("gitRepo")...) + allErrs = append(allErrs, validateGitRepoVolumeSource(source.GitRepo, fldPath.Child("gitRepo"))...) } if source.GCEPersistentDisk != nil { numVolumes++ - allErrs = append(allErrs, validateGCEPersistentDiskVolumeSource(source.GCEPersistentDisk).Prefix("persistentDisk")...) + allErrs = append(allErrs, validateGCEPersistentDiskVolumeSource(source.GCEPersistentDisk, fldPath.Child("persistentDisk"))...) } if source.AWSElasticBlockStore != nil { numVolumes++ - allErrs = append(allErrs, validateAWSElasticBlockStoreVolumeSource(source.AWSElasticBlockStore).Prefix("awsElasticBlockStore")...) + allErrs = append(allErrs, validateAWSElasticBlockStoreVolumeSource(source.AWSElasticBlockStore, fldPath.Child("awsElasticBlockStore"))...) } if source.Secret != nil { numVolumes++ - allErrs = append(allErrs, validateSecretVolumeSource(source.Secret).Prefix("secret")...) + allErrs = append(allErrs, validateSecretVolumeSource(source.Secret, fldPath.Child("secret"))...) } if source.NFS != nil { numVolumes++ - allErrs = append(allErrs, validateNFS(source.NFS).Prefix("nfs")...) + allErrs = append(allErrs, validateNFSVolumeSource(source.NFS, fldPath.Child("nfs"))...) } if source.ISCSI != nil { numVolumes++ - allErrs = append(allErrs, validateISCSIVolumeSource(source.ISCSI).Prefix("iscsi")...) + allErrs = append(allErrs, validateISCSIVolumeSource(source.ISCSI, fldPath.Child("iscsi"))...) } if source.Glusterfs != nil { numVolumes++ - allErrs = append(allErrs, validateGlusterfs(source.Glusterfs).Prefix("glusterfs")...) + allErrs = append(allErrs, validateGlusterfs(source.Glusterfs, fldPath.Child("glusterfs"))...) } if source.Flocker != nil { numVolumes++ - allErrs = append(allErrs, validateFlocker(source.Flocker).Prefix("flocker")...) + allErrs = append(allErrs, validateFlockerVolumeSource(source.Flocker, fldPath.Child("flocker"))...) } if source.PersistentVolumeClaim != nil { numVolumes++ - allErrs = append(allErrs, validatePersistentClaimVolumeSource(source.PersistentVolumeClaim).Prefix("persistentVolumeClaim")...) + allErrs = append(allErrs, validatePersistentClaimVolumeSource(source.PersistentVolumeClaim, fldPath.Child("persistentVolumeClaim"))...) } if source.RBD != nil { numVolumes++ - allErrs = append(allErrs, validateRBD(source.RBD).Prefix("rbd")...) + allErrs = append(allErrs, validateRBDVolumeSource(source.RBD, fldPath.Child("rbd"))...) } if source.Cinder != nil { numVolumes++ - allErrs = append(allErrs, validateCinderVolumeSource(source.Cinder).Prefix("cinder")...) + allErrs = append(allErrs, validateCinderVolumeSource(source.Cinder, fldPath.Child("cinder"))...) } if source.CephFS != nil { numVolumes++ - allErrs = append(allErrs, validateCephFS(source.CephFS).Prefix("cephfs")...) + allErrs = append(allErrs, validateCephFSVolumeSource(source.CephFS, fldPath.Child("cephfs"))...) } if source.DownwardAPI != nil { numVolumes++ - allErrs = append(allErrs, validateDownwardAPIVolumeSource(source.DownwardAPI).Prefix("downwardApi")...) + allErrs = append(allErrs, validateDownwardAPIVolumeSource(source.DownwardAPI, fldPath.Child("downwardAPI"))...) } if source.FC != nil { numVolumes++ - allErrs = append(allErrs, validateFCVolumeSource(source.FC).Prefix("fc")...) + allErrs = append(allErrs, validateFCVolumeSource(source.FC, fldPath.Child("fc"))...) } if numVolumes != 1 { - allErrs = append(allErrs, validation.NewInvalidError("", source, "exactly 1 volume type is required")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, source, "exactly 1 volume type is required")) } return allErrs } -func validateHostPathVolumeSource(hostPath *api.HostPathVolumeSource) validation.ErrorList { +func validateHostPathVolumeSource(hostPath *api.HostPathVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if hostPath.Path == "" { - allErrs = append(allErrs, validation.NewRequiredError("path")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("path"))) } return allErrs } -func validateGitRepoVolumeSource(gitRepo *api.GitRepoVolumeSource) validation.ErrorList { +func validateGitRepoVolumeSource(gitRepo *api.GitRepoVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(gitRepo.Repository) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("repository")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("repository"))) } - pathErrs := validateVolumeSourcePath(gitRepo.Directory, "directory") + pathErrs := validateVolumeSourcePath(gitRepo.Directory, fldPath.Child("directory")) allErrs = append(allErrs, pathErrs...) return allErrs } -func validateISCSIVolumeSource(iscsi *api.ISCSIVolumeSource) validation.ErrorList { +func validateISCSIVolumeSource(iscsi *api.ISCSIVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if iscsi.TargetPortal == "" { - allErrs = append(allErrs, validation.NewRequiredError("targetPortal")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("targetPortal"))) } if iscsi.IQN == "" { - allErrs = append(allErrs, validation.NewRequiredError("iqn")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("iqn"))) } if iscsi.FSType == "" { - allErrs = append(allErrs, validation.NewRequiredError("fsType")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("fsType"))) } if iscsi.Lun < 0 || iscsi.Lun > 255 { - allErrs = append(allErrs, validation.NewInvalidError("lun", iscsi.Lun, "")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("lun"), iscsi.Lun, "")) } return allErrs } -func validateFCVolumeSource(fc *api.FCVolumeSource) validation.ErrorList { +func validateFCVolumeSource(fc *api.FCVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(fc.TargetWWNs) < 1 { - allErrs = append(allErrs, validation.NewRequiredError("targetWWNs")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("targetWWNs"))) } if fc.FSType == "" { - allErrs = append(allErrs, validation.NewRequiredError("fsType")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("fsType"))) } if fc.Lun == nil { - allErrs = append(allErrs, validation.NewRequiredError("lun")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("lun"))) } else { if *fc.Lun < 0 || *fc.Lun > 255 { - allErrs = append(allErrs, validation.NewInvalidError("lun", fc.Lun, "")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("lun"), fc.Lun, "")) } } return allErrs } -func validateGCEPersistentDiskVolumeSource(PD *api.GCEPersistentDiskVolumeSource) validation.ErrorList { +func validateGCEPersistentDiskVolumeSource(pd *api.GCEPersistentDiskVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - if PD.PDName == "" { - allErrs = append(allErrs, validation.NewRequiredError("pdName")) + if pd.PDName == "" { + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("pdName"))) } - if PD.FSType == "" { - allErrs = append(allErrs, validation.NewRequiredError("fsType")) + if pd.FSType == "" { + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("fsType"))) } - if PD.Partition < 0 || PD.Partition > 255 { - allErrs = append(allErrs, validation.NewInvalidError("partition", PD.Partition, pdPartitionErrorMsg)) + if pd.Partition < 0 || pd.Partition > 255 { + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("partition"), pd.Partition, pdPartitionErrorMsg)) } return allErrs } -func validateAWSElasticBlockStoreVolumeSource(PD *api.AWSElasticBlockStoreVolumeSource) validation.ErrorList { +func validateAWSElasticBlockStoreVolumeSource(PD *api.AWSElasticBlockStoreVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if PD.VolumeID == "" { - allErrs = append(allErrs, validation.NewRequiredError("volumeID")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("volumeID"))) } if PD.FSType == "" { - allErrs = append(allErrs, validation.NewRequiredError("fsType")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("fsType"))) } if PD.Partition < 0 || PD.Partition > 255 { - allErrs = append(allErrs, validation.NewInvalidError("partition", PD.Partition, pdPartitionErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("partition"), PD.Partition, pdPartitionErrorMsg)) } return allErrs } -func validateSecretVolumeSource(secretSource *api.SecretVolumeSource) validation.ErrorList { +func validateSecretVolumeSource(secretSource *api.SecretVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if secretSource.SecretName == "" { - allErrs = append(allErrs, validation.NewRequiredError("secretName")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("secretName"))) } return allErrs } -func validatePersistentClaimVolumeSource(claim *api.PersistentVolumeClaimVolumeSource) validation.ErrorList { +func validatePersistentClaimVolumeSource(claim *api.PersistentVolumeClaimVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if claim.ClaimName == "" { - allErrs = append(allErrs, validation.NewRequiredError("claimName")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("claimName"))) } return allErrs } -func validateNFS(nfs *api.NFSVolumeSource) validation.ErrorList { +func validateNFSVolumeSource(nfs *api.NFSVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if nfs.Server == "" { - allErrs = append(allErrs, validation.NewRequiredError("server")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("server"))) } if nfs.Path == "" { - allErrs = append(allErrs, validation.NewRequiredError("path")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("path"))) } if !path.IsAbs(nfs.Path) { - allErrs = append(allErrs, validation.NewInvalidError("path", nfs.Path, "must be an absolute path")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("path"), nfs.Path, "must be an absolute path")) } return allErrs } -func validateGlusterfs(glusterfs *api.GlusterfsVolumeSource) validation.ErrorList { +func validateGlusterfs(glusterfs *api.GlusterfsVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if glusterfs.EndpointsName == "" { - allErrs = append(allErrs, validation.NewRequiredError("endpoints")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("endpoints"))) } if glusterfs.Path == "" { - allErrs = append(allErrs, validation.NewRequiredError("path")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("path"))) } return allErrs } -func validateFlocker(flocker *api.FlockerVolumeSource) validation.ErrorList { +func validateFlockerVolumeSource(flocker *api.FlockerVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if flocker.DatasetName == "" { - allErrs = append(allErrs, validation.NewRequiredError("datasetName")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("datasetName"))) } if strings.Contains(flocker.DatasetName, "/") { - allErrs = append(allErrs, validation.NewInvalidError("datasetName", flocker.DatasetName, "must not contain '/'")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("datasetName"), flocker.DatasetName, "must not contain '/'")) } return allErrs } var validDownwardAPIFieldPathExpressions = sets.NewString("metadata.name", "metadata.namespace", "metadata.labels", "metadata.annotations") -func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSource) validation.ErrorList { +func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} for _, downwardAPIVolumeFile := range downwardAPIVolume.Items { if len(downwardAPIVolumeFile.Path) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("path")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("path"))) } - allErrs = append(allErrs, validateVolumeSourcePath(downwardAPIVolumeFile.Path, "path")...) - allErrs = append(allErrs, validateObjectFieldSelector(&downwardAPIVolumeFile.FieldRef, &validDownwardAPIFieldPathExpressions).Prefix("FieldRef")...) + allErrs = append(allErrs, validateVolumeSourcePath(downwardAPIVolumeFile.Path, fldPath.Child("path"))...) + allErrs = append(allErrs, validateObjectFieldSelector(&downwardAPIVolumeFile.FieldRef, &validDownwardAPIFieldPathExpressions, fldPath.Child("fieldRef"))...) } return allErrs } @@ -583,53 +585,54 @@ func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSou // 1. is not abs path // 2. does not contain '..' // 3. does not start with '..' -func validateVolumeSourcePath(targetPath string, field string) validation.ErrorList { +func validateVolumeSourcePath(targetPath string, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if path.IsAbs(targetPath) { - allErrs = append(allErrs, validation.NewForbiddenError(field, "must not be an absolute path")) + allErrs = append(allErrs, validation.NewForbiddenError(fldPath, "must not be an absolute path")) } // TODO assume OS of api server & nodes are the same for now items := strings.Split(targetPath, string(os.PathSeparator)) for _, item := range items { if item == ".." { - allErrs = append(allErrs, validation.NewInvalidError(field, targetPath, "must not contain \"..\"")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, targetPath, "must not contain \"..\"")) } } if strings.HasPrefix(items[0], "..") && len(items[0]) > 2 { - allErrs = append(allErrs, validation.NewInvalidError(field, targetPath, "must not start with \"..\"")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, targetPath, "must not start with \"..\"")) } return allErrs } -func validateRBD(rbd *api.RBDVolumeSource) validation.ErrorList { + +func validateRBDVolumeSource(rbd *api.RBDVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(rbd.CephMonitors) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("monitors")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("monitors"))) } if rbd.RBDImage == "" { - allErrs = append(allErrs, validation.NewRequiredError("image")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("image"))) } if rbd.FSType == "" { - allErrs = append(allErrs, validation.NewRequiredError("fsType")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("fsType"))) } return allErrs } -func validateCinderVolumeSource(cd *api.CinderVolumeSource) validation.ErrorList { +func validateCinderVolumeSource(cd *api.CinderVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if cd.VolumeID == "" { - allErrs = append(allErrs, validation.NewRequiredError("volumeID")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("volumeID"))) } if cd.FSType == "" || (cd.FSType != "ext3" && cd.FSType != "ext4") { - allErrs = append(allErrs, validation.NewRequiredError("fsType required and should be of type ext3 or ext4")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("fsType"))) } return allErrs } -func validateCephFS(cephfs *api.CephFSVolumeSource) validation.ErrorList { +func validateCephFSVolumeSource(cephfs *api.CephFSVolumeSource, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(cephfs.Monitors) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("monitors")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("monitors"))) } return allErrs } @@ -638,79 +641,80 @@ func ValidatePersistentVolumeName(name string, prefix bool) (bool, string) { return NameIsDNSSubdomain(name, prefix) } +var supportedAccessModes = sets.NewString(string(api.ReadWriteOnce), string(api.ReadOnlyMany), string(api.ReadWriteMany)) + func ValidatePersistentVolume(pv *api.PersistentVolume) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&pv.ObjectMeta, false, ValidatePersistentVolumeName).Prefix("metadata")...) + allErrs := ValidateObjectMeta(&pv.ObjectMeta, false, ValidatePersistentVolumeName, validation.NewFieldPath("metadata")) + specPath := validation.NewFieldPath("spec") if len(pv.Spec.AccessModes) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("persistentVolume.AccessModes")) + allErrs = append(allErrs, validation.NewRequiredError(specPath.Child("accessModes"))) } - for _, mode := range pv.Spec.AccessModes { - if mode != api.ReadWriteOnce && mode != api.ReadOnlyMany && mode != api.ReadWriteMany { - allErrs = append(allErrs, validation.NewInvalidError("persistentVolume.Spec.AccessModes", mode, fmt.Sprintf("only %s, %s, and %s are valid", api.ReadWriteOnce, api.ReadOnlyMany, api.ReadWriteMany))) + if !supportedAccessModes.Has(string(mode)) { + allErrs = append(allErrs, validation.NewNotSupportedError(specPath.Child("accessModes"), mode, supportedAccessModes.List())) } } if len(pv.Spec.Capacity) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("persistentVolume.Capacity")) + allErrs = append(allErrs, validation.NewRequiredError(specPath.Child("capacity"))) } if _, ok := pv.Spec.Capacity[api.ResourceStorage]; !ok || len(pv.Spec.Capacity) > 1 { - allErrs = append(allErrs, validation.NewInvalidError("", pv.Spec.Capacity, fmt.Sprintf("only %s is expected", api.ResourceStorage))) + allErrs = append(allErrs, validation.NewNotSupportedError(specPath.Child("capacity"), pv.Spec.Capacity, []string{string(api.ResourceStorage)})) } - - for _, qty := range pv.Spec.Capacity { - allErrs = append(allErrs, validateBasicResource(qty)...) + capPath := specPath.Child("capacity") + for r, qty := range pv.Spec.Capacity { + allErrs = append(allErrs, validateBasicResource(qty, capPath.Key(string(r)))...) } numVolumes := 0 if pv.Spec.HostPath != nil { numVolumes++ - allErrs = append(allErrs, validateHostPathVolumeSource(pv.Spec.HostPath).Prefix("hostPath")...) + allErrs = append(allErrs, validateHostPathVolumeSource(pv.Spec.HostPath, specPath.Child("hostPath"))...) } if pv.Spec.GCEPersistentDisk != nil { numVolumes++ - allErrs = append(allErrs, validateGCEPersistentDiskVolumeSource(pv.Spec.GCEPersistentDisk).Prefix("persistentDisk")...) + allErrs = append(allErrs, validateGCEPersistentDiskVolumeSource(pv.Spec.GCEPersistentDisk, specPath.Child("persistentDisk"))...) } if pv.Spec.AWSElasticBlockStore != nil { numVolumes++ - allErrs = append(allErrs, validateAWSElasticBlockStoreVolumeSource(pv.Spec.AWSElasticBlockStore).Prefix("awsElasticBlockStore")...) + allErrs = append(allErrs, validateAWSElasticBlockStoreVolumeSource(pv.Spec.AWSElasticBlockStore, specPath.Child("awsElasticBlockStore"))...) } if pv.Spec.Glusterfs != nil { numVolumes++ - allErrs = append(allErrs, validateGlusterfs(pv.Spec.Glusterfs).Prefix("glusterfs")...) + allErrs = append(allErrs, validateGlusterfs(pv.Spec.Glusterfs, specPath.Child("glusterfs"))...) } if pv.Spec.Flocker != nil { numVolumes++ - allErrs = append(allErrs, validateFlocker(pv.Spec.Flocker).Prefix("flocker")...) + allErrs = append(allErrs, validateFlockerVolumeSource(pv.Spec.Flocker, specPath.Child("flocker"))...) } if pv.Spec.NFS != nil { numVolumes++ - allErrs = append(allErrs, validateNFS(pv.Spec.NFS).Prefix("nfs")...) + allErrs = append(allErrs, validateNFSVolumeSource(pv.Spec.NFS, specPath.Child("nfs"))...) } if pv.Spec.RBD != nil { numVolumes++ - allErrs = append(allErrs, validateRBD(pv.Spec.RBD).Prefix("rbd")...) + allErrs = append(allErrs, validateRBDVolumeSource(pv.Spec.RBD, specPath.Child("rbd"))...) } if pv.Spec.CephFS != nil { numVolumes++ - allErrs = append(allErrs, validateCephFS(pv.Spec.CephFS).Prefix("cephfs")...) + allErrs = append(allErrs, validateCephFSVolumeSource(pv.Spec.CephFS, specPath.Child("cephfs"))...) } if pv.Spec.ISCSI != nil { numVolumes++ - allErrs = append(allErrs, validateISCSIVolumeSource(pv.Spec.ISCSI).Prefix("iscsi")...) + allErrs = append(allErrs, validateISCSIVolumeSource(pv.Spec.ISCSI, specPath.Child("iscsi"))...) } if pv.Spec.Cinder != nil { numVolumes++ - allErrs = append(allErrs, validateCinderVolumeSource(pv.Spec.Cinder).Prefix("cinder")...) + allErrs = append(allErrs, validateCinderVolumeSource(pv.Spec.Cinder, specPath.Child("cinder"))...) } if pv.Spec.FC != nil { numVolumes++ - allErrs = append(allErrs, validateFCVolumeSource(pv.Spec.FC).Prefix("fc")...) + allErrs = append(allErrs, validateFCVolumeSource(pv.Spec.FC, specPath.Child("fc"))...) } if numVolumes != 1 { - allErrs = append(allErrs, validation.NewInvalidError("", pv.Spec.PersistentVolumeSource, "exactly 1 volume type is required")) + allErrs = append(allErrs, validation.NewInvalidError(specPath, pv.Spec.PersistentVolumeSource, "exactly 1 volume type is required")) } return allErrs } @@ -728,26 +732,27 @@ func ValidatePersistentVolumeUpdate(newPv, oldPv *api.PersistentVolume) validati // newPv is updated with fields that cannot be changed. func ValidatePersistentVolumeStatusUpdate(newPv, oldPv *api.PersistentVolume) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPv.ObjectMeta, &oldPv.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPv.ObjectMeta, &oldPv.ObjectMeta, validation.NewFieldPath("metadata"))...) if newPv.ResourceVersion == "" { - allErrs = append(allErrs, validation.NewRequiredError("resourceVersion")) + allErrs = append(allErrs, validation.NewRequiredError(validation.NewFieldPath("resourceVersion"))) } newPv.Spec = oldPv.Spec return allErrs } func ValidatePersistentVolumeClaim(pvc *api.PersistentVolumeClaim) validation.ErrorList { - allErrs := ValidateObjectMeta(&pvc.ObjectMeta, true, ValidatePersistentVolumeName) + allErrs := ValidateObjectMeta(&pvc.ObjectMeta, true, ValidatePersistentVolumeName, validation.NewFieldPath("metadata")) + specPath := validation.NewFieldPath("spec") if len(pvc.Spec.AccessModes) == 0 { - allErrs = append(allErrs, validation.NewInvalidError("persistentVolumeClaim.Spec.AccessModes", pvc.Spec.AccessModes, "at least 1 PersistentVolumeAccessMode is required")) + allErrs = append(allErrs, validation.NewInvalidError(specPath.Child("accessModes"), pvc.Spec.AccessModes, "at least 1 accessMode is required")) } for _, mode := range pvc.Spec.AccessModes { if mode != api.ReadWriteOnce && mode != api.ReadOnlyMany && mode != api.ReadWriteMany { - allErrs = append(allErrs, validation.NewInvalidError("persistentVolumeClaim.Spec.AccessModes", mode, fmt.Sprintf("only %s, %s, and %s are valid", api.ReadWriteOnce, api.ReadOnlyMany, api.ReadWriteMany))) + allErrs = append(allErrs, validation.NewNotSupportedError(specPath.Child("accessModes"), mode, supportedAccessModes.List())) } } if _, ok := pvc.Spec.Resources.Requests[api.ResourceStorage]; !ok { - allErrs = append(allErrs, validation.NewInvalidError("persistentVolumeClaim.Spec.Resources.Requests", pvc.Spec.Resources.Requests, "No Storage size specified")) + allErrs = append(allErrs, validation.NewRequiredError(specPath.Child("resources").Key(string(api.ResourceStorage)))) } return allErrs } @@ -761,15 +766,16 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *api.PersistentVolumeCla func ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc *api.PersistentVolumeClaim) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPvc.ObjectMeta, &oldPvc.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPvc.ObjectMeta, &oldPvc.ObjectMeta, validation.NewFieldPath("metadata"))...) if newPvc.ResourceVersion == "" { - allErrs = append(allErrs, validation.NewRequiredError("resourceVersion")) + allErrs = append(allErrs, validation.NewRequiredError(validation.NewFieldPath("resourceVersion"))) } if len(newPvc.Spec.AccessModes) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("persistentVolume.AccessModes")) + allErrs = append(allErrs, validation.NewRequiredError(validation.NewFieldPath("Spec", "accessModes"))) } - for _, qty := range newPvc.Status.Capacity { - allErrs = append(allErrs, validateBasicResource(qty)...) + capPath := validation.NewFieldPath("status", "capacity") + for r, qty := range newPvc.Status.Capacity { + allErrs = append(allErrs, validateBasicResource(qty, capPath.Key(string(r)))...) } newPvc.Spec = oldPvc.Spec return allErrs @@ -777,58 +783,56 @@ func ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc *api.PersistentVol var supportedPortProtocols = sets.NewString(string(api.ProtocolTCP), string(api.ProtocolUDP)) -func validatePorts(ports []api.ContainerPort) validation.ErrorList { +func validateContainerPorts(ports []api.ContainerPort, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} allNames := sets.String{} for i, port := range ports { - pErrs := validation.ErrorList{} + idxPath := fldPath.Index(i) if len(port.Name) > 0 { if !validation.IsValidPortName(port.Name) { - pErrs = append(pErrs, validation.NewInvalidError("name", port.Name, PortNameErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("name"), port.Name, PortNameErrorMsg)) } else if allNames.Has(port.Name) { - pErrs = append(pErrs, validation.NewDuplicateError("name", port.Name)) + allErrs = append(allErrs, validation.NewDuplicateError(idxPath.Child("name"), port.Name)) } else { allNames.Insert(port.Name) } } if port.ContainerPort == 0 { - pErrs = append(pErrs, validation.NewInvalidError("containerPort", port.ContainerPort, PortRangeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("containerPort"), port.ContainerPort, PortRangeErrorMsg)) } else if !validation.IsValidPortNum(port.ContainerPort) { - pErrs = append(pErrs, validation.NewInvalidError("containerPort", port.ContainerPort, PortRangeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("containerPort"), port.ContainerPort, PortRangeErrorMsg)) } if port.HostPort != 0 && !validation.IsValidPortNum(port.HostPort) { - pErrs = append(pErrs, validation.NewInvalidError("hostPort", port.HostPort, PortRangeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("hostPort"), port.HostPort, PortRangeErrorMsg)) } if len(port.Protocol) == 0 { - pErrs = append(pErrs, validation.NewRequiredError("protocol")) + allErrs = append(allErrs, validation.NewRequiredError(idxPath.Child("protocol"))) } else if !supportedPortProtocols.Has(string(port.Protocol)) { - pErrs = append(pErrs, validation.NewNotSupportedError("protocol", port.Protocol, supportedPortProtocols.List())) + allErrs = append(allErrs, validation.NewNotSupportedError(idxPath.Child("protocol"), port.Protocol, supportedPortProtocols.List())) } - allErrs = append(allErrs, pErrs.PrefixIndex(i)...) } return allErrs } -func validateEnv(vars []api.EnvVar) validation.ErrorList { +func validateEnv(vars []api.EnvVar, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} for i, ev := range vars { - vErrs := validation.ErrorList{} + idxPath := fldPath.Index(i) if len(ev.Name) == 0 { - vErrs = append(vErrs, validation.NewRequiredError("name")) + allErrs = append(allErrs, validation.NewRequiredError(idxPath.Child("name"))) } else if !validation.IsCIdentifier(ev.Name) { - vErrs = append(vErrs, validation.NewInvalidError("name", ev.Name, cIdentifierErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("name"), ev.Name, cIdentifierErrorMsg)) } - vErrs = append(vErrs, validateEnvVarValueFrom(ev).Prefix("valueFrom")...) - allErrs = append(allErrs, vErrs.PrefixIndex(i)...) + allErrs = append(allErrs, validateEnvVarValueFrom(ev, idxPath.Child("valueFrom"))...) } return allErrs } var validFieldPathExpressionsEnv = sets.NewString("metadata.name", "metadata.namespace", "status.podIP") -func validateEnvVarValueFrom(ev api.EnvVar) validation.ErrorList { +func validateEnvVarValueFrom(ev api.EnvVar, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if ev.ValueFrom == nil { @@ -840,265 +844,267 @@ func validateEnvVarValueFrom(ev api.EnvVar) validation.ErrorList { switch { case ev.ValueFrom.FieldRef != nil: numSources++ - allErrs = append(allErrs, validateObjectFieldSelector(ev.ValueFrom.FieldRef, &validFieldPathExpressionsEnv).Prefix("fieldRef")...) + allErrs = append(allErrs, validateObjectFieldSelector(ev.ValueFrom.FieldRef, &validFieldPathExpressionsEnv, fldPath.Child("fieldRef"))...) } if ev.Value != "" && numSources != 0 { - allErrs = append(allErrs, validation.NewInvalidError("", "", "sources cannot be specified when value is not empty")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, "", "sources cannot be specified when value is not empty")) } return allErrs } -func validateObjectFieldSelector(fs *api.ObjectFieldSelector, expressions *sets.String) validation.ErrorList { +func validateObjectFieldSelector(fs *api.ObjectFieldSelector, expressions *sets.String, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if fs.APIVersion == "" { - allErrs = append(allErrs, validation.NewRequiredError("apiVersion")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("apiVersion"))) } else if fs.FieldPath == "" { - allErrs = append(allErrs, validation.NewRequiredError("fieldPath")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("fieldPath"))) } else { internalFieldPath, _, err := api.Scheme.ConvertFieldLabel(fs.APIVersion, "Pod", fs.FieldPath, "") if err != nil { - allErrs = append(allErrs, validation.NewInvalidError("fieldPath", fs.FieldPath, "error converting fieldPath")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("fieldPath"), fs.FieldPath, "error converting fieldPath")) } else if !expressions.Has(internalFieldPath) { - allErrs = append(allErrs, validation.NewNotSupportedError("fieldPath", internalFieldPath, expressions.List())) + allErrs = append(allErrs, validation.NewNotSupportedError(fldPath.Child("fieldPath"), internalFieldPath, expressions.List())) } } return allErrs } -func validateVolumeMounts(mounts []api.VolumeMount, volumes sets.String) validation.ErrorList { +func validateVolumeMounts(mounts []api.VolumeMount, volumes sets.String, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} for i, mnt := range mounts { - mErrs := validation.ErrorList{} + idxPath := fldPath.Index(i) if len(mnt.Name) == 0 { - mErrs = append(mErrs, validation.NewRequiredError("name")) + allErrs = append(allErrs, validation.NewRequiredError(idxPath.Child("name"))) } else if !volumes.Has(mnt.Name) { - mErrs = append(mErrs, validation.NewNotFoundError("name", mnt.Name)) + allErrs = append(allErrs, validation.NewNotFoundError(idxPath.Child("name"), mnt.Name)) } if len(mnt.MountPath) == 0 { - mErrs = append(mErrs, validation.NewRequiredError("mountPath")) + allErrs = append(allErrs, validation.NewRequiredError(idxPath.Child("mountPath"))) } - allErrs = append(allErrs, mErrs.PrefixIndex(i)...) } return allErrs } -func validateProbe(probe *api.Probe) validation.ErrorList { +func validateProbe(probe *api.Probe, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if probe == nil { return allErrs } - allErrs = append(allErrs, validateHandler(&probe.Handler)...) + allErrs = append(allErrs, validateHandler(&probe.Handler, fldPath)...) - allErrs = append(allErrs, ValidatePositiveField(int64(probe.InitialDelaySeconds), "initialDelaySeconds")...) - allErrs = append(allErrs, ValidatePositiveField(int64(probe.TimeoutSeconds), "timeoutSeconds")...) - allErrs = append(allErrs, ValidatePositiveField(int64(probe.PeriodSeconds), "periodSeconds")...) - allErrs = append(allErrs, ValidatePositiveField(int64(probe.SuccessThreshold), "successThreshold")...) - allErrs = append(allErrs, ValidatePositiveField(int64(probe.FailureThreshold), "failureThreshold")...) + allErrs = append(allErrs, ValidatePositiveField(int64(probe.InitialDelaySeconds), fldPath.Child("initialDelaySeconds"))...) + allErrs = append(allErrs, ValidatePositiveField(int64(probe.TimeoutSeconds), fldPath.Child("timeoutSeconds"))...) + allErrs = append(allErrs, ValidatePositiveField(int64(probe.PeriodSeconds), fldPath.Child("periodSeconds"))...) + allErrs = append(allErrs, ValidatePositiveField(int64(probe.SuccessThreshold), fldPath.Child("successThreshold"))...) + allErrs = append(allErrs, ValidatePositiveField(int64(probe.FailureThreshold), fldPath.Child("failureThreshold"))...) return allErrs } // AccumulateUniqueHostPorts extracts each HostPort of each Container, // accumulating the results and returning an error if any ports conflict. -func AccumulateUniqueHostPorts(containers []api.Container, accumulator *sets.String) validation.ErrorList { +func AccumulateUniqueHostPorts(containers []api.Container, accumulator *sets.String, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} for ci, ctr := range containers { - cErrs := validation.ErrorList{} + idxPath := fldPath.Index(ci) + portsPath := idxPath.Child("ports") for pi := range ctr.Ports { + idxPath := portsPath.Index(pi) port := ctr.Ports[pi].HostPort if port == 0 { continue } str := fmt.Sprintf("%d/%s", port, ctr.Ports[pi].Protocol) if accumulator.Has(str) { - cErrs = append(cErrs, validation.NewDuplicateError("port", str)) + allErrs = append(allErrs, validation.NewDuplicateError(idxPath.Child("hostPort"), str)) } else { accumulator.Insert(str) } } - allErrs = append(allErrs, cErrs.PrefixIndex(ci)...) } return allErrs } // checkHostPortConflicts checks for colliding Port.HostPort values across // a slice of containers. -func checkHostPortConflicts(containers []api.Container) validation.ErrorList { +func checkHostPortConflicts(containers []api.Container, fldPath *validation.FieldPath) validation.ErrorList { allPorts := sets.String{} - return AccumulateUniqueHostPorts(containers, &allPorts) + return AccumulateUniqueHostPorts(containers, &allPorts, fldPath) } -func validateExecAction(exec *api.ExecAction) validation.ErrorList { +func validateExecAction(exec *api.ExecAction, fldPath *validation.FieldPath) validation.ErrorList { allErrors := validation.ErrorList{} if len(exec.Command) == 0 { - allErrors = append(allErrors, validation.NewRequiredError("command")) + allErrors = append(allErrors, validation.NewRequiredError(fldPath.Child("command"))) } return allErrors } -func validateHTTPGetAction(http *api.HTTPGetAction) validation.ErrorList { +func validateHTTPGetAction(http *api.HTTPGetAction, fldPath *validation.FieldPath) validation.ErrorList { allErrors := validation.ErrorList{} if len(http.Path) == 0 { - allErrors = append(allErrors, validation.NewRequiredError("path")) + allErrors = append(allErrors, validation.NewRequiredError(fldPath.Child("path"))) } if http.Port.Type == intstr.Int && !validation.IsValidPortNum(http.Port.IntValue()) { - allErrors = append(allErrors, validation.NewInvalidError("port", http.Port, PortRangeErrorMsg)) + allErrors = append(allErrors, validation.NewInvalidError(fldPath.Child("port"), http.Port, PortRangeErrorMsg)) } else if http.Port.Type == intstr.String && !validation.IsValidPortName(http.Port.StrVal) { - allErrors = append(allErrors, validation.NewInvalidError("port", http.Port.StrVal, PortNameErrorMsg)) + allErrors = append(allErrors, validation.NewInvalidError(fldPath.Child("port"), http.Port.StrVal, PortNameErrorMsg)) } supportedSchemes := sets.NewString(string(api.URISchemeHTTP), string(api.URISchemeHTTPS)) if !supportedSchemes.Has(string(http.Scheme)) { - allErrors = append(allErrors, validation.NewInvalidError("scheme", http.Scheme, fmt.Sprintf("must be one of %v", supportedSchemes.List()))) + allErrors = append(allErrors, validation.NewInvalidError(fldPath.Child("scheme"), http.Scheme, fmt.Sprintf("must be one of %v", supportedSchemes.List()))) } return allErrors } -func validateTCPSocketAction(tcp *api.TCPSocketAction) validation.ErrorList { +func validateTCPSocketAction(tcp *api.TCPSocketAction, fldPath *validation.FieldPath) validation.ErrorList { allErrors := validation.ErrorList{} if tcp.Port.Type == intstr.Int && !validation.IsValidPortNum(tcp.Port.IntValue()) { - allErrors = append(allErrors, validation.NewInvalidError("port", tcp.Port, PortRangeErrorMsg)) + allErrors = append(allErrors, validation.NewInvalidError(fldPath.Child("port"), tcp.Port, PortRangeErrorMsg)) } else if tcp.Port.Type == intstr.String && !validation.IsValidPortName(tcp.Port.StrVal) { - allErrors = append(allErrors, validation.NewInvalidError("port", tcp.Port.StrVal, PortNameErrorMsg)) + allErrors = append(allErrors, validation.NewInvalidError(fldPath.Child("port"), tcp.Port.StrVal, PortNameErrorMsg)) } return allErrors } -func validateHandler(handler *api.Handler) validation.ErrorList { +func validateHandler(handler *api.Handler, fldPath *validation.FieldPath) validation.ErrorList { numHandlers := 0 allErrors := validation.ErrorList{} if handler.Exec != nil { numHandlers++ - allErrors = append(allErrors, validateExecAction(handler.Exec).Prefix("exec")...) + allErrors = append(allErrors, validateExecAction(handler.Exec, fldPath.Child("exec"))...) } if handler.HTTPGet != nil { numHandlers++ - allErrors = append(allErrors, validateHTTPGetAction(handler.HTTPGet).Prefix("httpGet")...) + allErrors = append(allErrors, validateHTTPGetAction(handler.HTTPGet, fldPath.Child("httpGet"))...) } if handler.TCPSocket != nil { numHandlers++ - allErrors = append(allErrors, validateTCPSocketAction(handler.TCPSocket).Prefix("tcpSocket")...) + allErrors = append(allErrors, validateTCPSocketAction(handler.TCPSocket, fldPath.Child("tcpSocket"))...) } if numHandlers != 1 { - allErrors = append(allErrors, validation.NewInvalidError("", handler, "exactly 1 handler type is required")) + allErrors = append(allErrors, validation.NewInvalidError(fldPath, handler, "exactly 1 handler type is required")) } return allErrors } -func validateLifecycle(lifecycle *api.Lifecycle) validation.ErrorList { +func validateLifecycle(lifecycle *api.Lifecycle, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if lifecycle.PostStart != nil { - allErrs = append(allErrs, validateHandler(lifecycle.PostStart).Prefix("postStart")...) + allErrs = append(allErrs, validateHandler(lifecycle.PostStart, fldPath.Child("postStart"))...) } if lifecycle.PreStop != nil { - allErrs = append(allErrs, validateHandler(lifecycle.PreStop).Prefix("preStop")...) + allErrs = append(allErrs, validateHandler(lifecycle.PreStop, fldPath.Child("preStop"))...) } return allErrs } -func validatePullPolicy(ctr *api.Container) validation.ErrorList { +var supportedPullPolicies = sets.NewString(string(api.PullAlways), string(api.PullIfNotPresent), string(api.PullNever)) + +func validatePullPolicy(policy api.PullPolicy, fldPath *validation.FieldPath) validation.ErrorList { allErrors := validation.ErrorList{} - switch ctr.ImagePullPolicy { + switch policy { case api.PullAlways, api.PullIfNotPresent, api.PullNever: break case "": - allErrors = append(allErrors, validation.NewRequiredError("")) + allErrors = append(allErrors, validation.NewRequiredError(fldPath)) default: - validValues := []string{string(api.PullAlways), string(api.PullIfNotPresent), string(api.PullNever)} - allErrors = append(allErrors, validation.NewNotSupportedError("", ctr.ImagePullPolicy, validValues)) + allErrors = append(allErrors, validation.NewNotSupportedError(fldPath, policy, supportedPullPolicies.List())) } return allErrors } -func validateContainers(containers []api.Container, volumes sets.String) validation.ErrorList { +func validateContainers(containers []api.Container, volumes sets.String, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(containers) == 0 { - return append(allErrs, validation.NewRequiredError("")) + return append(allErrs, validation.NewRequiredError(fldPath)) } allNames := sets.String{} for i, ctr := range containers { - cErrs := validation.ErrorList{} + idxPath := fldPath.Index(i) if len(ctr.Name) == 0 { - cErrs = append(cErrs, validation.NewRequiredError("name")) + allErrs = append(allErrs, validation.NewRequiredError(idxPath.Child("name"))) } else if !validation.IsDNS1123Label(ctr.Name) { - cErrs = append(cErrs, validation.NewInvalidError("name", ctr.Name, DNS1123LabelErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("name"), ctr.Name, DNS1123LabelErrorMsg)) } else if allNames.Has(ctr.Name) { - cErrs = append(cErrs, validation.NewDuplicateError("name", ctr.Name)) + allErrs = append(allErrs, validation.NewDuplicateError(idxPath.Child("name"), ctr.Name)) } else { allNames.Insert(ctr.Name) } if len(ctr.Image) == 0 { - cErrs = append(cErrs, validation.NewRequiredError("image")) + allErrs = append(allErrs, validation.NewRequiredError(idxPath.Child("image"))) } if ctr.Lifecycle != nil { - cErrs = append(cErrs, validateLifecycle(ctr.Lifecycle).Prefix("lifecycle")...) + allErrs = append(allErrs, validateLifecycle(ctr.Lifecycle, idxPath.Child("lifecycle"))...) } - cErrs = append(cErrs, validateProbe(ctr.LivenessProbe).Prefix("livenessProbe")...) + allErrs = append(allErrs, validateProbe(ctr.LivenessProbe, idxPath.Child("livenessProbe"))...) // Liveness-specific validation if ctr.LivenessProbe != nil && ctr.LivenessProbe.SuccessThreshold != 1 { - allErrs = append(allErrs, validation.NewForbiddenError("livenessProbe.successThreshold", "must be 1")) + allErrs = append(allErrs, validation.NewForbiddenError(idxPath.Child("livenessProbe", "successThreshold"), "must be 1")) } - cErrs = append(cErrs, validateProbe(ctr.ReadinessProbe).Prefix("readinessProbe")...) - cErrs = append(cErrs, validatePorts(ctr.Ports).Prefix("ports")...) - cErrs = append(cErrs, validateEnv(ctr.Env).Prefix("env")...) - cErrs = append(cErrs, validateVolumeMounts(ctr.VolumeMounts, volumes).Prefix("volumeMounts")...) - cErrs = append(cErrs, validatePullPolicy(&ctr).Prefix("imagePullPolicy")...) - cErrs = append(cErrs, ValidateResourceRequirements(&ctr.Resources).Prefix("resources")...) - cErrs = append(cErrs, ValidateSecurityContext(ctr.SecurityContext).Prefix("securityContext")...) - allErrs = append(allErrs, cErrs.PrefixIndex(i)...) + allErrs = append(allErrs, validateProbe(ctr.ReadinessProbe, idxPath.Child("readinessProbe"))...) + allErrs = append(allErrs, validateContainerPorts(ctr.Ports, idxPath.Child("ports"))...) + allErrs = append(allErrs, validateEnv(ctr.Env, idxPath.Child("env"))...) + allErrs = append(allErrs, validateVolumeMounts(ctr.VolumeMounts, volumes, idxPath.Child("volumeMounts"))...) + allErrs = append(allErrs, validatePullPolicy(ctr.ImagePullPolicy, idxPath.Child("imagePullPolicy"))...) + allErrs = append(allErrs, ValidateResourceRequirements(&ctr.Resources, idxPath.Child("resources"))...) + allErrs = append(allErrs, ValidateSecurityContext(ctr.SecurityContext, idxPath.Child("securityContext"))...) } // Check for colliding ports across all containers. - allErrs = append(allErrs, checkHostPortConflicts(containers)...) + allErrs = append(allErrs, checkHostPortConflicts(containers, fldPath)...) return allErrs } -func validateRestartPolicy(restartPolicy *api.RestartPolicy) validation.ErrorList { +func validateRestartPolicy(restartPolicy *api.RestartPolicy, fldPath *validation.FieldPath) validation.ErrorList { allErrors := validation.ErrorList{} switch *restartPolicy { case api.RestartPolicyAlways, api.RestartPolicyOnFailure, api.RestartPolicyNever: break case "": - allErrors = append(allErrors, validation.NewRequiredError("")) + allErrors = append(allErrors, validation.NewRequiredError(fldPath)) default: validValues := []string{string(api.RestartPolicyAlways), string(api.RestartPolicyOnFailure), string(api.RestartPolicyNever)} - allErrors = append(allErrors, validation.NewNotSupportedError("", *restartPolicy, validValues)) + allErrors = append(allErrors, validation.NewNotSupportedError(fldPath, *restartPolicy, validValues)) } return allErrors } -func validateDNSPolicy(dnsPolicy *api.DNSPolicy) validation.ErrorList { +func validateDNSPolicy(dnsPolicy *api.DNSPolicy, fldPath *validation.FieldPath) validation.ErrorList { allErrors := validation.ErrorList{} switch *dnsPolicy { case api.DNSClusterFirst, api.DNSDefault: break case "": - allErrors = append(allErrors, validation.NewRequiredError("")) + allErrors = append(allErrors, validation.NewRequiredError(fldPath)) default: validValues := []string{string(api.DNSClusterFirst), string(api.DNSDefault)} - allErrors = append(allErrors, validation.NewNotSupportedError("", dnsPolicy, validValues)) + allErrors = append(allErrors, validation.NewNotSupportedError(fldPath, dnsPolicy, validValues)) } return allErrors } -func validateHostNetwork(hostNetwork bool, containers []api.Container) validation.ErrorList { +func validateHostNetwork(hostNetwork bool, containers []api.Container, fldPath *validation.FieldPath) validation.ErrorList { allErrors := validation.ErrorList{} if hostNetwork { - for _, container := range containers { - for _, port := range container.Ports { + for i, container := range containers { + portsPath := fldPath.Index(i).Child("ports") + for i, port := range container.Ports { + idxPath := portsPath.Index(i) if port.HostPort != port.ContainerPort { - allErrors = append(allErrors, validation.NewInvalidError("containerPort", port.ContainerPort, "containerPort must match hostPort if hostNetwork is set to true")) + allErrors = append(allErrors, validation.NewInvalidError(idxPath.Child("containerPort"), port.ContainerPort, "must match hostPort when hostNetwork is set to true")) } } } @@ -1106,15 +1112,17 @@ func validateHostNetwork(hostNetwork bool, containers []api.Container) validatio return allErrors } -// validateImagePullSecrets checks to make sure the pull secrets are well formed. Right now, we only expect name to be set (it's the only field). If this ever changes -// and someone decides to set those fields, we'd like to know. -func validateImagePullSecrets(imagePullSecrets []api.LocalObjectReference) validation.ErrorList { +// validateImagePullSecrets checks to make sure the pull secrets are well +// formed. Right now, we only expect name to be set (it's the only field). If +// this ever changes and someone decides to set those fields, we'd like to +// know. +func validateImagePullSecrets(imagePullSecrets []api.LocalObjectReference, fldPath *validation.FieldPath) validation.ErrorList { allErrors := validation.ErrorList{} for i, currPullSecret := range imagePullSecrets { + idxPath := fldPath.Index(i) strippedRef := api.LocalObjectReference{Name: currPullSecret.Name} - if !reflect.DeepEqual(strippedRef, currPullSecret) { - allErrors = append(allErrors, validation.NewInvalidError(fmt.Sprintf("[%d]", i), currPullSecret, "only name may be set")) + allErrors = append(allErrors, validation.NewInvalidError(idxPath, currPullSecret, "only name may be set")) } } return allErrors @@ -1122,10 +1130,8 @@ func validateImagePullSecrets(imagePullSecrets []api.LocalObjectReference) valid // ValidatePod tests if required fields in the pod are set. func ValidatePod(pod *api.Pod) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName).Prefix("metadata")...) - allErrs = append(allErrs, ValidatePodSpec(&pod.Spec).Prefix("spec")...) - + allErrs := ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName, validation.NewFieldPath("metadata")) + allErrs = append(allErrs, ValidatePodSpec(&pod.Spec, validation.NewFieldPath("spec"))...) return allErrs } @@ -1133,37 +1139,37 @@ func ValidatePod(pod *api.Pod) validation.ErrorList { // This includes checking formatting and uniqueness. It also canonicalizes the // structure by setting default values and implementing any backwards-compatibility // tricks. -func ValidatePodSpec(spec *api.PodSpec) validation.ErrorList { +func ValidatePodSpec(spec *api.PodSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allVolumes, vErrs := validateVolumes(spec.Volumes) - allErrs = append(allErrs, vErrs.Prefix("volumes")...) - allErrs = append(allErrs, validateContainers(spec.Containers, allVolumes).Prefix("containers")...) - allErrs = append(allErrs, validateRestartPolicy(&spec.RestartPolicy).Prefix("restartPolicy")...) - allErrs = append(allErrs, validateDNSPolicy(&spec.DNSPolicy).Prefix("dnsPolicy")...) - allErrs = append(allErrs, ValidateLabels(spec.NodeSelector, "nodeSelector")...) - allErrs = append(allErrs, ValidatePodSecurityContext(spec.SecurityContext, spec).Prefix("securityContext")...) - allErrs = append(allErrs, validateImagePullSecrets(spec.ImagePullSecrets).Prefix("imagePullSecrets")...) + allVolumes, vErrs := validateVolumes(spec.Volumes, fldPath.Child("volumes")) + allErrs = append(allErrs, vErrs...) + allErrs = append(allErrs, validateContainers(spec.Containers, allVolumes, fldPath.Child("containers"))...) + allErrs = append(allErrs, validateRestartPolicy(&spec.RestartPolicy, fldPath.Child("restartPolicy"))...) + allErrs = append(allErrs, validateDNSPolicy(&spec.DNSPolicy, fldPath.Child("dnsPolicy"))...) + allErrs = append(allErrs, ValidateLabels(spec.NodeSelector, fldPath.Child("nodeSelector"))...) + allErrs = append(allErrs, ValidatePodSecurityContext(spec.SecurityContext, spec, fldPath, fldPath.Child("securityContext"))...) + allErrs = append(allErrs, validateImagePullSecrets(spec.ImagePullSecrets, fldPath.Child("imagePullSecrets"))...) if len(spec.ServiceAccountName) > 0 { if ok, msg := ValidateServiceAccountName(spec.ServiceAccountName, false); !ok { - allErrs = append(allErrs, validation.NewInvalidError("serviceAccountName", spec.ServiceAccountName, msg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("serviceAccountName"), spec.ServiceAccountName, msg)) } } if spec.ActiveDeadlineSeconds != nil { if *spec.ActiveDeadlineSeconds <= 0 { - allErrs = append(allErrs, validation.NewInvalidError("activeDeadlineSeconds", spec.ActiveDeadlineSeconds, "activeDeadlineSeconds must be a positive integer greater than 0")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("activeDeadlineSeconds"), spec.ActiveDeadlineSeconds, "must be greater than 0")) } } return allErrs } // ValidatePodSecurityContext test that the specified PodSecurityContext has valid data. -func ValidatePodSecurityContext(securityContext *api.PodSecurityContext, spec *api.PodSpec) validation.ErrorList { +func ValidatePodSecurityContext(securityContext *api.PodSecurityContext, spec *api.PodSpec, specPath, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if securityContext != nil { - allErrs = append(allErrs, validateHostNetwork(securityContext.HostNetwork, spec.Containers).Prefix("hostNetwork")...) + allErrs = append(allErrs, validateHostNetwork(securityContext.HostNetwork, spec.Containers, specPath.Child("containers"))...) } return allErrs @@ -1174,11 +1180,12 @@ func ValidatePodSecurityContext(securityContext *api.PodSecurityContext, spec *a func ValidatePodUpdate(newPod, oldPod *api.Pod) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPod.ObjectMeta, &oldPod.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPod.ObjectMeta, &oldPod.ObjectMeta, validation.NewFieldPath("metadata"))...) + specPath := validation.NewFieldPath("spec") if len(newPod.Spec.Containers) != len(oldPod.Spec.Containers) { //TODO: Pinpoint the specific container that causes the invalid error after we have strategic merge diff - allErrs = append(allErrs, validation.NewInvalidError("spec.containers", "content of spec.containers is not printed out, please refer to the \"details\"", "may not add or remove containers")) + allErrs = append(allErrs, validation.NewInvalidError(specPath.Child("containers"), "contents not printed here, please refer to the \"details\"", "may not add or remove containers")) return allErrs } pod := *newPod @@ -1191,7 +1198,7 @@ func ValidatePodUpdate(newPod, oldPod *api.Pod) validation.ErrorList { pod.Spec.Containers = newContainers if !api.Semantic.DeepEqual(pod.Spec, oldPod.Spec) { //TODO: Pinpoint the specific field that causes the invalid error after we have strategic merge diff - allErrs = append(allErrs, validation.NewInvalidError("spec", "content of spec is not printed out, please refer to the \"details\"", "may not update fields other than container.image")) + allErrs = append(allErrs, validation.NewInvalidError(specPath, "contents not printed here, please refer to the \"details\"", "may not update fields other than container.image")) } newPod.Status = oldPod.Status @@ -1203,11 +1210,11 @@ func ValidatePodUpdate(newPod, oldPod *api.Pod) validation.ErrorList { func ValidatePodStatusUpdate(newPod, oldPod *api.Pod) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPod.ObjectMeta, &oldPod.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPod.ObjectMeta, &oldPod.ObjectMeta, validation.NewFieldPath("metadata"))...) // TODO: allow change when bindings are properly decoupled from pods if newPod.Spec.NodeName != oldPod.Spec.NodeName { - allErrs = append(allErrs, validation.NewInvalidError("status.nodeName", newPod.Spec.NodeName, "pod nodename cannot be changed directly")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("status", "nodeName"), newPod.Spec.NodeName, "cannot be changed directly")) } // For status update we ignore changes to pod spec. @@ -1218,9 +1225,8 @@ func ValidatePodStatusUpdate(newPod, oldPod *api.Pod) validation.ErrorList { // ValidatePodTemplate tests if required fields in the pod template are set. func ValidatePodTemplate(pod *api.PodTemplate) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName).Prefix("metadata")...) - allErrs = append(allErrs, ValidatePodTemplateSpec(&pod.Template).Prefix("template")...) + allErrs := ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName, validation.NewFieldPath("metadata")) + allErrs = append(allErrs, ValidatePodTemplateSpec(&pod.Template, validation.NewFieldPath("template"))...) return allErrs } @@ -1228,8 +1234,8 @@ func ValidatePodTemplate(pod *api.PodTemplate) validation.ErrorList { // that cannot be changed. func ValidatePodTemplateUpdate(newPod, oldPod *api.PodTemplate) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldPod.ObjectMeta, &newPod.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidatePodTemplateSpec(&newPod.Template).Prefix("template")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldPod.ObjectMeta, &newPod.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&newPod.Template, validation.NewFieldPath("template"))...) return allErrs } @@ -1239,85 +1245,96 @@ var supportedServiceType = sets.NewString(string(api.ServiceTypeClusterIP), stri // ValidateService tests if required fields in the service are set. func ValidateService(service *api.Service) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName).Prefix("metadata")...) + allErrs := ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName, validation.NewFieldPath("metadata")) + specPath := validation.NewFieldPath("spec") if len(service.Spec.Ports) == 0 && service.Spec.ClusterIP != api.ClusterIPNone { - allErrs = append(allErrs, validation.NewRequiredError("spec.ports")) + allErrs = append(allErrs, validation.NewRequiredError(specPath.Child("ports"))) } if service.Spec.Type == api.ServiceTypeLoadBalancer { for ix := range service.Spec.Ports { port := &service.Spec.Ports[ix] if port.Port == 10250 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.ports[%d].port", ix), port.Port, "can not expose port 10250 externally since it is used by kubelet")) + portPath := specPath.Child("ports").Index(ix) + allErrs = append(allErrs, validation.NewInvalidError(portPath, port.Port, "can not expose port 10250 externally since it is used by kubelet")) } } } isHeadlessService := service.Spec.ClusterIP == api.ClusterIPNone allPortNames := sets.String{} + portsPath := specPath.Child("ports") for i := range service.Spec.Ports { - allErrs = append(allErrs, validateServicePort(&service.Spec.Ports[i], len(service.Spec.Ports) > 1, isHeadlessService, &allPortNames).PrefixIndex(i).Prefix("spec.ports")...) + portPath := portsPath.Index(i) + allErrs = append(allErrs, validateServicePort(&service.Spec.Ports[i], len(service.Spec.Ports) > 1, isHeadlessService, &allPortNames, portPath)...) } if service.Spec.Selector != nil { - allErrs = append(allErrs, ValidateLabels(service.Spec.Selector, "spec.selector")...) + allErrs = append(allErrs, ValidateLabels(service.Spec.Selector, specPath.Child("selector"))...) } if service.Spec.SessionAffinity == "" { - allErrs = append(allErrs, validation.NewRequiredError("spec.sessionAffinity")) + allErrs = append(allErrs, validation.NewRequiredError(specPath.Child("sessionAffinity"))) } else if !supportedSessionAffinityType.Has(string(service.Spec.SessionAffinity)) { - allErrs = append(allErrs, validation.NewNotSupportedError("spec.sessionAffinity", service.Spec.SessionAffinity, supportedSessionAffinityType.List())) + allErrs = append(allErrs, validation.NewNotSupportedError(specPath.Child("sessionAffinity"), service.Spec.SessionAffinity, supportedSessionAffinityType.List())) } if api.IsServiceIPSet(service) { if ip := net.ParseIP(service.Spec.ClusterIP); ip == nil { - allErrs = append(allErrs, validation.NewInvalidError("spec.clusterIP", service.Spec.ClusterIP, "clusterIP should be empty, 'None', or a valid IP address")) + allErrs = append(allErrs, validation.NewInvalidError(specPath.Child("clusterIP"), service.Spec.ClusterIP, "must be empty, 'None', or a valid IP address")) } } - for _, ip := range service.Spec.ExternalIPs { + ipPath := specPath.Child("externalIPs") + for i, ip := range service.Spec.ExternalIPs { + idxPath := ipPath.Index(i) if ip == "0.0.0.0" { - allErrs = append(allErrs, validation.NewInvalidError("spec.externalIPs", ip, "is not an IP address")) + allErrs = append(allErrs, validation.NewInvalidError(idxPath, ip, "is not an IP address")) } - allErrs = append(allErrs, validateIpIsNotLinkLocalOrLoopback(ip, "spec.externalIPs")...) + allErrs = append(allErrs, validateIpIsNotLinkLocalOrLoopback(ip, idxPath)...) } if service.Spec.Type == "" { - allErrs = append(allErrs, validation.NewRequiredError("spec.type")) + allErrs = append(allErrs, validation.NewRequiredError(specPath.Child("type"))) } else if !supportedServiceType.Has(string(service.Spec.Type)) { - allErrs = append(allErrs, validation.NewNotSupportedError("spec.type", service.Spec.Type, supportedServiceType.List())) + allErrs = append(allErrs, validation.NewNotSupportedError(specPath.Child("type"), service.Spec.Type, supportedServiceType.List())) } if service.Spec.Type == api.ServiceTypeLoadBalancer { + portsPath := specPath.Child("ports") for i := range service.Spec.Ports { + portPath := portsPath.Index(i) if service.Spec.Ports[i].Protocol != api.ProtocolTCP { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.ports[%d].protocol", i), service.Spec.Ports[i].Protocol, "cannot create an external load balancer with non-TCP ports")) + allErrs = append(allErrs, validation.NewInvalidError(portPath.Child("protocol"), service.Spec.Ports[i].Protocol, "cannot create an external load balancer with non-TCP ports")) } } } if service.Spec.Type == api.ServiceTypeClusterIP { + portsPath := specPath.Child("ports") for i := range service.Spec.Ports { + portPath := portsPath.Index(i) if service.Spec.Ports[i].NodePort != 0 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.ports[%d].nodePort", i), service.Spec.Ports[i].NodePort, "cannot specify a node port with services of type ClusterIP")) + allErrs = append(allErrs, validation.NewInvalidError(portPath.Child("nodePort"), service.Spec.Ports[i].NodePort, "cannot specify a node port with services of type ClusterIP")) } } } // Check for duplicate NodePorts, considering (protocol,port) pairs + portsPath = specPath.Child("ports") nodePorts := make(map[api.ServicePort]bool) for i := range service.Spec.Ports { port := &service.Spec.Ports[i] if port.NodePort == 0 { continue } + portPath := portsPath.Index(i) var key api.ServicePort key.Protocol = port.Protocol key.NodePort = port.NodePort _, found := nodePorts[key] if found { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.ports[%d].nodePort", i), port.NodePort, "duplicate nodePort specified")) + allErrs = append(allErrs, validation.NewInvalidError(portPath.Child("nodePort"), port.NodePort, "duplicate nodePort specified")) } nodePorts[key] = true } @@ -1325,41 +1342,41 @@ func ValidateService(service *api.Service) validation.ErrorList { return allErrs } -func validateServicePort(sp *api.ServicePort, requireName, isHeadlessService bool, allNames *sets.String) validation.ErrorList { +func validateServicePort(sp *api.ServicePort, requireName, isHeadlessService bool, allNames *sets.String, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if requireName && sp.Name == "" { - allErrs = append(allErrs, validation.NewRequiredError("name")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("name"))) } else if sp.Name != "" { if !validation.IsDNS1123Label(sp.Name) { - allErrs = append(allErrs, validation.NewInvalidError("name", sp.Name, DNS1123LabelErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("name"), sp.Name, DNS1123LabelErrorMsg)) } else if allNames.Has(sp.Name) { - allErrs = append(allErrs, validation.NewDuplicateError("name", sp.Name)) + allErrs = append(allErrs, validation.NewDuplicateError(fldPath.Child("name"), sp.Name)) } else { allNames.Insert(sp.Name) } } if !validation.IsValidPortNum(sp.Port) { - allErrs = append(allErrs, validation.NewInvalidError("port", sp.Port, PortRangeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("port"), sp.Port, PortRangeErrorMsg)) } if len(sp.Protocol) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("protocol")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("protocol"))) } else if !supportedPortProtocols.Has(string(sp.Protocol)) { - allErrs = append(allErrs, validation.NewNotSupportedError("protocol", sp.Protocol, supportedPortProtocols.List())) + allErrs = append(allErrs, validation.NewNotSupportedError(fldPath.Child("protocol"), sp.Protocol, supportedPortProtocols.List())) } if sp.TargetPort.Type == intstr.Int && !validation.IsValidPortNum(sp.TargetPort.IntValue()) { - allErrs = append(allErrs, validation.NewInvalidError("targetPort", sp.TargetPort, PortRangeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("targetPort"), sp.TargetPort, PortRangeErrorMsg)) } if sp.TargetPort.Type == intstr.String && !validation.IsValidPortName(sp.TargetPort.StrVal) { - allErrs = append(allErrs, validation.NewInvalidError("targetPort", sp.TargetPort, PortNameErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("targetPort"), sp.TargetPort, PortNameErrorMsg)) } if isHeadlessService { if sp.TargetPort.Type == intstr.String || (sp.TargetPort.Type == intstr.Int && sp.Port != sp.TargetPort.IntValue()) { - allErrs = append(allErrs, validation.NewInvalidError("port", sp.Port, "must be equal to targetPort when clusterIP = None")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("port"), sp.Port, "must be equal to targetPort when clusterIP = None")) } } @@ -1369,10 +1386,10 @@ func validateServicePort(sp *api.ServicePort, requireName, isHeadlessService boo // ValidateServiceUpdate tests if required fields in the service are set during an update func ValidateServiceUpdate(service, oldService *api.Service) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, validation.NewFieldPath("metadata"))...) if api.IsServiceIPSet(oldService) { - allErrs = append(allErrs, ValidateImmutableField(service.Spec.ClusterIP, oldService.Spec.ClusterIP, "spec.clusterIP")...) + allErrs = append(allErrs, ValidateImmutableField(service.Spec.ClusterIP, oldService.Spec.ClusterIP, validation.NewFieldPath("spec", "clusterIP"))...) } allErrs = append(allErrs, ValidateService(service)...) @@ -1381,90 +1398,92 @@ func ValidateServiceUpdate(service, oldService *api.Service) validation.ErrorLis // ValidateReplicationController tests if required fields in the replication controller are set. func ValidateReplicationController(controller *api.ReplicationController) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&controller.ObjectMeta, true, ValidateReplicationControllerName).Prefix("metadata")...) - allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec).Prefix("spec")...) + allErrs := ValidateObjectMeta(&controller.ObjectMeta, true, ValidateReplicationControllerName, validation.NewFieldPath("metadata")) + allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec, validation.NewFieldPath("spec"))...) return allErrs } // ValidateReplicationControllerUpdate tests if required fields in the replication controller are set. func ValidateReplicationControllerUpdate(controller, oldController *api.ReplicationController) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec).Prefix("spec")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec, validation.NewFieldPath("spec"))...) return allErrs } // ValidateReplicationControllerStatusUpdate tests if required fields in the replication controller are set. func ValidateReplicationControllerStatusUpdate(controller, oldController *api.ReplicationController) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidatePositiveField(int64(controller.Status.Replicas), "status.replicas")...) - allErrs = append(allErrs, ValidatePositiveField(int64(controller.Status.ObservedGeneration), "status.observedGeneration")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta, validation.NewFieldPath("metadata"))...) + statusPath := validation.NewFieldPath("status") + allErrs = append(allErrs, ValidatePositiveField(int64(controller.Status.Replicas), statusPath.Child("replicas"))...) + allErrs = append(allErrs, ValidatePositiveField(int64(controller.Status.ObservedGeneration), statusPath.Child("observedGeneration"))...) return allErrs } // Validates that the given selector is non-empty. -func ValidateNonEmptySelector(selectorMap map[string]string, fieldName string) validation.ErrorList { +func ValidateNonEmptySelector(selectorMap map[string]string, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} selector := labels.Set(selectorMap).AsSelector() if selector.Empty() { - allErrs = append(allErrs, validation.NewRequiredError(fieldName)) + allErrs = append(allErrs, validation.NewRequiredError(fldPath)) } return allErrs } // Validates the given template and ensures that it is in accordance with the desrired selector and replicas. -func ValidatePodTemplateSpecForRC(template *api.PodTemplateSpec, selectorMap map[string]string, replicas int, fieldName string) validation.ErrorList { +func ValidatePodTemplateSpecForRC(template *api.PodTemplateSpec, selectorMap map[string]string, replicas int, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if template == nil { - allErrs = append(allErrs, validation.NewRequiredError(fieldName)) + allErrs = append(allErrs, validation.NewRequiredError(fldPath)) } else { selector := labels.Set(selectorMap).AsSelector() if !selector.Empty() { // Verify that the RC selector matches the labels in template. labels := labels.Set(template.Labels) if !selector.Matches(labels) { - allErrs = append(allErrs, validation.NewInvalidError(fieldName+".metadata.labels", template.Labels, "selector does not match labels in "+fieldName)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("metadata", "labels"), template.Labels, "selector does not match labels in "+fldPath.String())) } } - allErrs = append(allErrs, ValidatePodTemplateSpec(template).Prefix(fieldName)...) + allErrs = append(allErrs, ValidatePodTemplateSpec(template, fldPath)...) if replicas > 1 { - allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(template.Spec.Volumes).Prefix(fieldName+".spec.volumes")...) + allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(template.Spec.Volumes, fldPath.Child("spec", "volumes"))...) } // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). if template.Spec.RestartPolicy != api.RestartPolicyAlways { - allErrs = append(allErrs, validation.NewNotSupportedError(fieldName+".spec.restartPolicy", template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) + allErrs = append(allErrs, validation.NewNotSupportedError(fldPath.Child("spec", "restartPolicy"), template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) } } return allErrs } // ValidateReplicationControllerSpec tests if required fields in the replication controller spec are set. -func ValidateReplicationControllerSpec(spec *api.ReplicationControllerSpec) validation.ErrorList { +func ValidateReplicationControllerSpec(spec *api.ReplicationControllerSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateNonEmptySelector(spec.Selector, "selector")...) - allErrs = append(allErrs, ValidatePositiveField(int64(spec.Replicas), "replicas")...) - allErrs = append(allErrs, ValidatePodTemplateSpecForRC(spec.Template, spec.Selector, spec.Replicas, "template")...) + allErrs = append(allErrs, ValidateNonEmptySelector(spec.Selector, fldPath.Child("selector"))...) + allErrs = append(allErrs, ValidatePositiveField(int64(spec.Replicas), fldPath.Child("replicas"))...) + allErrs = append(allErrs, ValidatePodTemplateSpecForRC(spec.Template, spec.Selector, spec.Replicas, fldPath.Child("template"))...) return allErrs } // ValidatePodTemplateSpec validates the spec of a pod template -func ValidatePodTemplateSpec(spec *api.PodTemplateSpec) validation.ErrorList { +func ValidatePodTemplateSpec(spec *api.PodTemplateSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateLabels(spec.Labels, "labels")...) - allErrs = append(allErrs, ValidateAnnotations(spec.Annotations, "annotations")...) - allErrs = append(allErrs, ValidatePodSpec(&spec.Spec).Prefix("spec")...) + allErrs = append(allErrs, ValidateLabels(spec.Labels, fldPath.Child("labels"))...) + allErrs = append(allErrs, ValidateAnnotations(spec.Annotations, fldPath.Child("annotations"))...) + allErrs = append(allErrs, ValidatePodSpec(&spec.Spec, fldPath.Child("spec"))...) return allErrs } -func ValidateReadOnlyPersistentDisks(volumes []api.Volume) validation.ErrorList { +func ValidateReadOnlyPersistentDisks(volumes []api.Volume, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - for _, vol := range volumes { + for i := range volumes { + vol := &volumes[i] + idxPath := fldPath.Index(i) if vol.GCEPersistentDisk != nil { if vol.GCEPersistentDisk.ReadOnly == false { - allErrs = append(allErrs, validation.NewInvalidError("GCEPersistentDisk.ReadOnly", false, "ReadOnly must be true for replicated pods > 1, as GCE PD can only be mounted on multiple machines if it is read-only.")) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("gcePersistentDisk", ".readOnly"), false, "readOnly must be true for replicated pods > 1, as GCE PD can only be mounted on multiple machines if it is read-only.")) } } // TODO: What to do for AWS? It doesn't support replicas @@ -1474,14 +1493,13 @@ func ValidateReadOnlyPersistentDisks(volumes []api.Volume) validation.ErrorList // ValidateNode tests if required fields in the node are set. func ValidateNode(node *api.Node) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&node.ObjectMeta, false, ValidateNodeName).Prefix("metadata")...) + allErrs := ValidateObjectMeta(&node.ObjectMeta, false, ValidateNodeName, validation.NewFieldPath("metadata")) // Only validate spec. All status fields are optional and can be updated later. // external ID is required. if len(node.Spec.ExternalID) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("spec.ExternalID")) + allErrs = append(allErrs, validation.NewRequiredError(validation.NewFieldPath("spec", "externalID"))) } // TODO(rjnagal): Ignore PodCIDR till its completely implemented. @@ -1491,7 +1509,7 @@ func ValidateNode(node *api.Node) validation.ErrorList { // ValidateNodeUpdate tests to make sure a node update can be applied. Modifies oldNode. func ValidateNodeUpdate(node, oldNode *api.Node) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&node.ObjectMeta, &oldNode.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&node.ObjectMeta, &oldNode.ObjectMeta, validation.NewFieldPath("metadata"))...) // TODO: Enable the code once we have better api object.status update model. Currently, // anyone can update node status. @@ -1501,9 +1519,9 @@ func ValidateNodeUpdate(node, oldNode *api.Node) validation.ErrorList { // Validte no duplicate addresses in node status. addresses := make(map[api.NodeAddress]bool) - for _, address := range node.Status.Addresses { + for i, address := range node.Status.Addresses { if _, ok := addresses[address]; ok { - allErrs = append(allErrs, validation.NewDuplicateError("addresses", address)) + allErrs = append(allErrs, validation.NewDuplicateError(validation.NewFieldPath("status", "addresses").Index(i), address)) } addresses[address] = true } @@ -1523,7 +1541,7 @@ func ValidateNodeUpdate(node, oldNode *api.Node) validation.ErrorList { // TODO: Add a 'real' error type for this error and provide print actual diffs. if !api.Semantic.DeepEqual(oldNode, node) { glog.V(4).Infof("Update failed validation %#v vs %#v", oldNode, node) - allErrs = append(allErrs, validation.NewForbiddenError("", "update contains more than labels or capacity changes")) + allErrs = append(allErrs, validation.NewForbiddenError(validation.NewFieldPath(""), "update contains more than labels or capacity changes")) } return allErrs @@ -1531,15 +1549,15 @@ func ValidateNodeUpdate(node, oldNode *api.Node) validation.ErrorList { // Validate compute resource typename. // Refer to docs/design/resources.md for more details. -func validateResourceName(value string, field string) validation.ErrorList { +func validateResourceName(value string, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if !validation.IsQualifiedName(value) { - return append(allErrs, validation.NewInvalidError(field, value, "resource typename: "+qualifiedNameErrorMsg)) + return append(allErrs, validation.NewInvalidError(fldPath, value, "resource typename: "+qualifiedNameErrorMsg)) } if len(strings.Split(value, "/")) == 1 { if !api.IsStandardResourceName(value) { - return append(allErrs, validation.NewInvalidError(field, value, "is neither a standard resource type nor is fully qualified")) + return append(allErrs, validation.NewInvalidError(fldPath, value, "is neither a standard resource type nor is fully qualified")) } } @@ -1548,16 +1566,17 @@ func validateResourceName(value string, field string) validation.ErrorList { // ValidateLimitRange tests if required fields in the LimitRange are set. func ValidateLimitRange(limitRange *api.LimitRange) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&limitRange.ObjectMeta, true, ValidateLimitRangeName).Prefix("metadata")...) + allErrs := ValidateObjectMeta(&limitRange.ObjectMeta, true, ValidateLimitRangeName, validation.NewFieldPath("metadata")) // ensure resource names are properly qualified per docs/design/resources.md limitTypeSet := map[api.LimitType]bool{} + fldPath := validation.NewFieldPath("spec", "limits") for i := range limitRange.Spec.Limits { - limit := limitRange.Spec.Limits[i] + idxPath := fldPath.Index(i) + limit := &limitRange.Spec.Limits[i] _, found := limitTypeSet[limit.Type] if found { - allErrs = append(allErrs, validation.NewDuplicateError(fmt.Sprintf("spec.limits[%d].type", i), limit.Type)) + allErrs = append(allErrs, validation.NewDuplicateError(idxPath.Child("type"), limit.Type)) } limitTypeSet[limit.Type] = true @@ -1569,38 +1588,38 @@ func ValidateLimitRange(limitRange *api.LimitRange) validation.ErrorList { maxLimitRequestRatios := map[string]resource.Quantity{} for k, q := range limit.Max { - allErrs = append(allErrs, validateResourceName(string(k), fmt.Sprintf("spec.limits[%d].max[%s]", i, k))...) + allErrs = append(allErrs, validateResourceName(string(k), idxPath.Child("max").Key(string(k)))...) keys.Insert(string(k)) max[string(k)] = q } for k, q := range limit.Min { - allErrs = append(allErrs, validateResourceName(string(k), fmt.Sprintf("spec.limits[%d].min[%s]", i, k))...) + allErrs = append(allErrs, validateResourceName(string(k), idxPath.Child("min").Key(string(k)))...) keys.Insert(string(k)) min[string(k)] = q } if limit.Type == api.LimitTypePod { if len(limit.Default) > 0 { - allErrs = append(allErrs, validation.NewInvalidError("spec.limits[%d].default", limit.Default, "Default is not supported when limit type is Pod")) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("default"), limit.Default, "not supported when limit type is Pod")) } if len(limit.DefaultRequest) > 0 { - allErrs = append(allErrs, validation.NewInvalidError("spec.limits[%d].defaultRequest", limit.DefaultRequest, "DefaultRequest is not supported when limit type is Pod")) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("defaultRequest"), limit.DefaultRequest, "not supported when limit type is Pod")) } } else { for k, q := range limit.Default { - allErrs = append(allErrs, validateResourceName(string(k), fmt.Sprintf("spec.limits[%d].default[%s]", i, k))...) + allErrs = append(allErrs, validateResourceName(string(k), idxPath.Child("default").Key(string(k)))...) keys.Insert(string(k)) defaults[string(k)] = q } for k, q := range limit.DefaultRequest { - allErrs = append(allErrs, validateResourceName(string(k), fmt.Sprintf("spec.limits[%d].defaultRequest[%s]", i, k))...) + allErrs = append(allErrs, validateResourceName(string(k), idxPath.Child("defaultRequest").Key(string(k)))...) keys.Insert(string(k)) defaultRequests[string(k)] = q } } for k, q := range limit.MaxLimitRequestRatio { - allErrs = append(allErrs, validateResourceName(string(k), fmt.Sprintf("spec.limits[%d].maxLimitRequestRatio[%s]", i, k))...) + allErrs = append(allErrs, validateResourceName(string(k), idxPath.Child("maxLimitRequestRatio").Key(string(k)))...) keys.Insert(string(k)) maxLimitRequestRatios[string(k)] = q } @@ -1613,30 +1632,30 @@ func ValidateLimitRange(limitRange *api.LimitRange) validation.ErrorList { maxRatio, maxRatioFound := maxLimitRequestRatios[k] if minQuantityFound && maxQuantityFound && minQuantity.Cmp(maxQuantity) > 0 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.limits[%d].min[%s]", i, k), minQuantity, fmt.Sprintf("min value %s is greater than max value %s", minQuantity.String(), maxQuantity.String()))) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("min").Key(string(k)), minQuantity, fmt.Sprintf("min value %s is greater than max value %s", minQuantity.String(), maxQuantity.String()))) } if defaultRequestQuantityFound && minQuantityFound && minQuantity.Cmp(defaultRequestQuantity) > 0 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.limits[%d].defaultRequest[%s]", i, k), defaultRequestQuantity, fmt.Sprintf("min value %s is greater than default request value %s", minQuantity.String(), defaultRequestQuantity.String()))) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("defaultRequest").Key(string(k)), defaultRequestQuantity, fmt.Sprintf("min value %s is greater than default request value %s", minQuantity.String(), defaultRequestQuantity.String()))) } if defaultRequestQuantityFound && maxQuantityFound && defaultRequestQuantity.Cmp(maxQuantity) > 0 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.limits[%d].defaultRequest[%s]", i, k), defaultRequestQuantity, fmt.Sprintf("default request value %s is greater than max value %s", defaultRequestQuantity.String(), maxQuantity.String()))) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("defaultRequest").Key(string(k)), defaultRequestQuantity, fmt.Sprintf("default request value %s is greater than max value %s", defaultRequestQuantity.String(), maxQuantity.String()))) } if defaultRequestQuantityFound && defaultQuantityFound && defaultRequestQuantity.Cmp(defaultQuantity) > 0 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.limits[%d].defaultRequest[%s]", i, k), defaultRequestQuantity, fmt.Sprintf("default request value %s is greater than default limit value %s", defaultRequestQuantity.String(), defaultQuantity.String()))) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("defaultRequest").Key(string(k)), defaultRequestQuantity, fmt.Sprintf("default request value %s is greater than default limit value %s", defaultRequestQuantity.String(), defaultQuantity.String()))) } if defaultQuantityFound && minQuantityFound && minQuantity.Cmp(defaultQuantity) > 0 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.limits[%d].default[%s]", i, k), minQuantity, fmt.Sprintf("min value %s is greater than default value %s", minQuantity.String(), defaultQuantity.String()))) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("default").Key(string(k)), minQuantity, fmt.Sprintf("min value %s is greater than default value %s", minQuantity.String(), defaultQuantity.String()))) } if defaultQuantityFound && maxQuantityFound && defaultQuantity.Cmp(maxQuantity) > 0 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.limits[%d].default[%s]", i, k), maxQuantity, fmt.Sprintf("default value %s is greater than max value %s", defaultQuantity.String(), maxQuantity.String()))) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("default").Key(string(k)), maxQuantity, fmt.Sprintf("default value %s is greater than max value %s", defaultQuantity.String(), maxQuantity.String()))) } if maxRatioFound && maxRatio.Cmp(*resource.NewQuantity(1, resource.DecimalSI)) < 0 { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.limits[%d].maxLimitRequestRatio[%s]", i, k), maxRatio, fmt.Sprintf("maxLimitRequestRatio %s is less than 1", maxRatio.String()))) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("maxLimitRequestRatio").Key(string(k)), maxRatio, fmt.Sprintf("ratio %s is less than 1", maxRatio.String()))) } if maxRatioFound && minQuantityFound && maxQuantityFound { maxRatioValue := float64(maxRatio.Value()) @@ -1649,7 +1668,7 @@ func ValidateLimitRange(limitRange *api.LimitRange) validation.ErrorList { } maxRatioLimit := float64(maxQuantityValue) / float64(minQuantityValue) if maxRatioValue > maxRatioLimit { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("spec.limits[%d].maxLimitRequestRatio[%s]", i, k), maxRatio, fmt.Sprintf("maxLimitRequestRatio %s is greater than max/min = %f", maxRatio.String(), maxRatioLimit))) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("maxLimitRequestRatio").Key(string(k)), maxRatio, fmt.Sprintf("ratio %s is greater than max/min = %f", maxRatio.String(), maxRatioLimit))) } } } @@ -1660,15 +1679,14 @@ func ValidateLimitRange(limitRange *api.LimitRange) validation.ErrorList { // ValidateServiceAccount tests if required fields in the ServiceAccount are set. func ValidateServiceAccount(serviceAccount *api.ServiceAccount) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&serviceAccount.ObjectMeta, true, ValidateServiceAccountName).Prefix("metadata")...) + allErrs := ValidateObjectMeta(&serviceAccount.ObjectMeta, true, ValidateServiceAccountName, validation.NewFieldPath("metadata")) return allErrs } // ValidateServiceAccountUpdate tests if required fields in the ServiceAccount are set. func ValidateServiceAccountUpdate(newServiceAccount, oldServiceAccount *api.ServiceAccount) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newServiceAccount.ObjectMeta, &oldServiceAccount.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newServiceAccount.ObjectMeta, &oldServiceAccount.ObjectMeta, validation.NewFieldPath("metadata"))...) allErrs = append(allErrs, ValidateServiceAccount(newServiceAccount)...) return allErrs } @@ -1685,20 +1703,18 @@ func IsSecretKey(value string) bool { // ValidateSecret tests if required fields in the Secret are set. func ValidateSecret(secret *api.Secret) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&secret.ObjectMeta, true, ValidateSecretName).Prefix("metadata")...) + allErrs := ValidateObjectMeta(&secret.ObjectMeta, true, ValidateSecretName, validation.NewFieldPath("metadata")) + dataPath := validation.NewFieldPath("data") totalSize := 0 for key, value := range secret.Data { if !IsSecretKey(key) { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("data[%s]", key), key, fmt.Sprintf("must have at most %d characters and match regex %s", validation.DNS1123SubdomainMaxLength, SecretKeyFmt))) + allErrs = append(allErrs, validation.NewInvalidError(dataPath.Key(key), key, fmt.Sprintf("must have at most %d characters and match regex %s", validation.DNS1123SubdomainMaxLength, SecretKeyFmt))) } - totalSize += len(value) } - if totalSize > api.MaxSecretSize { - allErrs = append(allErrs, validation.NewForbiddenError("data", "Maximum secret size exceeded")) + allErrs = append(allErrs, validation.NewTooLongError(dataPath, "", api.MaxSecretSize)) } switch secret.Type { @@ -1706,20 +1722,20 @@ func ValidateSecret(secret *api.Secret) validation.ErrorList { // Only require Annotations[kubernetes.io/service-account.name] // Additional fields (like Annotations[kubernetes.io/service-account.uid] and Data[token]) might be contributed later by a controller loop if value := secret.Annotations[api.ServiceAccountNameKey]; len(value) == 0 { - allErrs = append(allErrs, validation.NewRequiredError(fmt.Sprintf("metadata.annotations[%s]", api.ServiceAccountNameKey))) + allErrs = append(allErrs, validation.NewRequiredError(validation.NewFieldPath("metadata", "annotations").Key(api.ServiceAccountNameKey))) } case api.SecretTypeOpaque, "": // no-op case api.SecretTypeDockercfg: dockercfgBytes, exists := secret.Data[api.DockerConfigKey] if !exists { - allErrs = append(allErrs, validation.NewRequiredError(fmt.Sprintf("data[%s]", api.DockerConfigKey))) + allErrs = append(allErrs, validation.NewRequiredError(dataPath.Key(api.DockerConfigKey))) break } // make sure that the content is well-formed json. if err := json.Unmarshal(dockercfgBytes, &map[string]interface{}{}); err != nil { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("data[%s]", api.DockerConfigKey), "", err.Error())) + allErrs = append(allErrs, validation.NewInvalidError(dataPath.Key(api.DockerConfigKey), "", err.Error())) } default: @@ -1732,33 +1748,35 @@ func ValidateSecret(secret *api.Secret) validation.ErrorList { // ValidateSecretUpdate tests if required fields in the Secret are set. func ValidateSecretUpdate(newSecret, oldSecret *api.Secret) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newSecret.ObjectMeta, &oldSecret.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newSecret.ObjectMeta, &oldSecret.ObjectMeta, validation.NewFieldPath("metadata"))...) if len(newSecret.Type) == 0 { newSecret.Type = oldSecret.Type } - allErrs = append(allErrs, ValidateImmutableField(newSecret.Type, oldSecret.Type, "type")...) + allErrs = append(allErrs, ValidateImmutableField(newSecret.Type, oldSecret.Type, validation.NewFieldPath("type"))...) allErrs = append(allErrs, ValidateSecret(newSecret)...) return allErrs } -func validateBasicResource(quantity resource.Quantity) validation.ErrorList { +func validateBasicResource(quantity resource.Quantity, fldPath *validation.FieldPath) validation.ErrorList { if quantity.Value() < 0 { - return validation.ErrorList{validation.NewInvalidError("", quantity.Value(), "must be a valid resource quantity")} + return validation.ErrorList{validation.NewInvalidError(fldPath, quantity.Value(), "must be a valid resource quantity")} } return validation.ErrorList{} } // Validates resource requirement spec. -func ValidateResourceRequirements(requirements *api.ResourceRequirements) validation.ErrorList { +func ValidateResourceRequirements(requirements *api.ResourceRequirements, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} + limPath := fldPath.Child("limits") for resourceName, quantity := range requirements.Limits { + fldPath := limPath.Key(string(resourceName)) // Validate resource name. - allErrs = append(allErrs, validateResourceName(resourceName.String(), fmt.Sprintf("resources.limits[%s]", resourceName))...) - if api.IsStandardResourceName(resourceName.String()) { - allErrs = append(allErrs, validateBasicResource(quantity).Prefix(fmt.Sprintf("Resource %s: ", resourceName))...) + allErrs = append(allErrs, validateResourceName(string(resourceName), fldPath)...) + if api.IsStandardResourceName(string(resourceName)) { + allErrs = append(allErrs, validateBasicResource(quantity, fldPath.Key(string(resourceName)))...) } // Check that request <= limit. requestQuantity, exists := requirements.Requests[resourceName] @@ -1772,15 +1790,17 @@ func ValidateResourceRequirements(requirements *api.ResourceRequirements) valida limitValue = quantity.MilliValue() } if limitValue < requestValue { - allErrs = append(allErrs, validation.NewInvalidError(fmt.Sprintf("resources.limits[%s]", resourceName), quantity.String(), "limit cannot be smaller than request")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, quantity.String(), "limit cannot be smaller than request")) } } } + reqPath := fldPath.Child("requests") for resourceName, quantity := range requirements.Requests { + fldPath := reqPath.Key(string(resourceName)) // Validate resource name. - allErrs = append(allErrs, validateResourceName(resourceName.String(), fmt.Sprintf("resources.requests[%s]", resourceName))...) - if api.IsStandardResourceName(resourceName.String()) { - allErrs = append(allErrs, validateBasicResource(quantity).Prefix(fmt.Sprintf("Resource %s: ", resourceName))...) + allErrs = append(allErrs, validateResourceName(string(resourceName), fldPath)...) + if api.IsStandardResourceName(string(resourceName)) { + allErrs = append(allErrs, validateBasicResource(quantity, fldPath.Key(string(resourceName)))...) } } return allErrs @@ -1788,31 +1808,36 @@ func ValidateResourceRequirements(requirements *api.ResourceRequirements) valida // ValidateResourceQuota tests if required fields in the ResourceQuota are set. func ValidateResourceQuota(resourceQuota *api.ResourceQuota) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&resourceQuota.ObjectMeta, true, ValidateResourceQuotaName).Prefix("metadata")...) + allErrs := ValidateObjectMeta(&resourceQuota.ObjectMeta, true, ValidateResourceQuotaName, validation.NewFieldPath("metadata")) + fldPath := validation.NewFieldPath("spec", "hard") for k, v := range resourceQuota.Spec.Hard { - allErrs = append(allErrs, validateResourceName(string(k), string(resourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) + resPath := fldPath.Key(string(k)) + allErrs = append(allErrs, validateResourceName(string(k), resPath)...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v, resPath)...) } + fldPath = validation.NewFieldPath("status", "hard") for k, v := range resourceQuota.Status.Hard { - allErrs = append(allErrs, validateResourceName(string(k), string(resourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) + resPath := fldPath.Key(string(k)) + allErrs = append(allErrs, validateResourceName(string(k), resPath)...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v, resPath)...) } + fldPath = validation.NewFieldPath("status", "used") for k, v := range resourceQuota.Status.Used { - allErrs = append(allErrs, validateResourceName(string(k), string(resourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) + resPath := fldPath.Key(string(k)) + allErrs = append(allErrs, validateResourceName(string(k), resPath)...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v, resPath)...) } return allErrs } // validateResourceQuantityValue enforces that specified quantity is valid for specified resource -func validateResourceQuantityValue(resource string, value resource.Quantity) validation.ErrorList { +func validateResourceQuantityValue(resource string, value resource.Quantity, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidatePositiveQuantity(value, resource)...) + allErrs = append(allErrs, ValidatePositiveQuantity(value, fldPath)...) if api.IsIntegerResourceName(resource) { if value.MilliValue()%int64(1000) != int64(0) { - allErrs = append(allErrs, validation.NewInvalidError(resource, value, isNotIntegerErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, value, isNotIntegerErrorMsg)) } } return allErrs @@ -1822,10 +1847,12 @@ func validateResourceQuantityValue(resource string, value resource.Quantity) val // newResourceQuota is updated with fields that cannot be changed. func ValidateResourceQuotaUpdate(newResourceQuota, oldResourceQuota *api.ResourceQuota) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newResourceQuota.ObjectMeta, &oldResourceQuota.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newResourceQuota.ObjectMeta, &oldResourceQuota.ObjectMeta, validation.NewFieldPath("metadata"))...) + fldPath := validation.NewFieldPath("spec", "hard") for k, v := range newResourceQuota.Spec.Hard { - allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) + resPath := fldPath.Key(string(k)) + allErrs = append(allErrs, validateResourceName(string(k), resPath)...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v, resPath)...) } newResourceQuota.Status = oldResourceQuota.Status return allErrs @@ -1835,17 +1862,21 @@ func ValidateResourceQuotaUpdate(newResourceQuota, oldResourceQuota *api.Resourc // newResourceQuota is updated with fields that cannot be changed. func ValidateResourceQuotaStatusUpdate(newResourceQuota, oldResourceQuota *api.ResourceQuota) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newResourceQuota.ObjectMeta, &oldResourceQuota.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newResourceQuota.ObjectMeta, &oldResourceQuota.ObjectMeta, validation.NewFieldPath("metadata"))...) if newResourceQuota.ResourceVersion == "" { - allErrs = append(allErrs, validation.NewRequiredError("resourceVersion")) + allErrs = append(allErrs, validation.NewRequiredError(validation.NewFieldPath("resourceVersion"))) } + fldPath := validation.NewFieldPath("status", "hard") for k, v := range newResourceQuota.Status.Hard { - allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) + resPath := fldPath.Key(string(k)) + allErrs = append(allErrs, validateResourceName(string(k), resPath)...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v, resPath)...) } + fldPath = validation.NewFieldPath("status", "used") for k, v := range newResourceQuota.Status.Used { - allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) + resPath := fldPath.Key(string(k)) + allErrs = append(allErrs, validateResourceName(string(k), resPath)...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v, resPath)...) } newResourceQuota.Spec = oldResourceQuota.Spec return allErrs @@ -1853,24 +1884,23 @@ func ValidateResourceQuotaStatusUpdate(newResourceQuota, oldResourceQuota *api.R // ValidateNamespace tests if required fields are set. func ValidateNamespace(namespace *api.Namespace) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&namespace.ObjectMeta, false, ValidateNamespaceName).Prefix("metadata")...) + allErrs := ValidateObjectMeta(&namespace.ObjectMeta, false, ValidateNamespaceName, validation.NewFieldPath("metadata")) for i := range namespace.Spec.Finalizers { - allErrs = append(allErrs, validateFinalizerName(string(namespace.Spec.Finalizers[i]))...) + allErrs = append(allErrs, validateFinalizerName(string(namespace.Spec.Finalizers[i]), validation.NewFieldPath("spec", "finalizers"))...) } return allErrs } // Validate finalizer names -func validateFinalizerName(stringValue string) validation.ErrorList { +func validateFinalizerName(stringValue string, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if !validation.IsQualifiedName(stringValue) { - return append(allErrs, validation.NewInvalidError("spec.finalizers", stringValue, qualifiedNameErrorMsg)) + return append(allErrs, validation.NewInvalidError(fldPath, stringValue, qualifiedNameErrorMsg)) } if len(strings.Split(stringValue, "/")) == 1 { if !api.IsStandardFinalizerName(stringValue) { - return append(allErrs, validation.NewInvalidError("spec.finalizers", stringValue, fmt.Sprintf("finalizer name is neither a standard finalizer name nor is it fully qualified"))) + return append(allErrs, validation.NewInvalidError(fldPath, stringValue, fmt.Sprintf("name is neither a standard finalizer name nor is it fully qualified"))) } } @@ -1881,7 +1911,7 @@ func validateFinalizerName(stringValue string) validation.ErrorList { // newNamespace is updated with fields that cannot be changed func ValidateNamespaceUpdate(newNamespace *api.Namespace, oldNamespace *api.Namespace) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newNamespace.ObjectMeta, &oldNamespace.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newNamespace.ObjectMeta, &oldNamespace.ObjectMeta, validation.NewFieldPath("metadata"))...) newNamespace.Spec.Finalizers = oldNamespace.Spec.Finalizers newNamespace.Status = oldNamespace.Status return allErrs @@ -1891,15 +1921,15 @@ func ValidateNamespaceUpdate(newNamespace *api.Namespace, oldNamespace *api.Name // that cannot be changed. func ValidateNamespaceStatusUpdate(newNamespace, oldNamespace *api.Namespace) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newNamespace.ObjectMeta, &oldNamespace.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newNamespace.ObjectMeta, &oldNamespace.ObjectMeta, validation.NewFieldPath("metadata"))...) newNamespace.Spec = oldNamespace.Spec if newNamespace.DeletionTimestamp.IsZero() { if newNamespace.Status.Phase != api.NamespaceActive { - allErrs = append(allErrs, validation.NewInvalidError("Status.Phase", newNamespace.Status.Phase, "A namespace may only be in active status if it does not have a deletion timestamp.")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("status", "Phase"), newNamespace.Status.Phase, "may only be in active status if it does not have a deletion timestamp.")) } } else { if newNamespace.Status.Phase != api.NamespaceTerminating { - allErrs = append(allErrs, validation.NewInvalidError("Status.Phase", newNamespace.Status.Phase, "A namespace may only be in terminating status if it has a deletion timestamp.")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("status", "Phase"), newNamespace.Status.Phase, "may only be in terminating status if it has a deletion timestamp.")) } } return allErrs @@ -1909,9 +1939,12 @@ func ValidateNamespaceStatusUpdate(newNamespace, oldNamespace *api.Namespace) va // newNamespace is updated with fields that cannot be changed. func ValidateNamespaceFinalizeUpdate(newNamespace, oldNamespace *api.Namespace) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newNamespace.ObjectMeta, &oldNamespace.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newNamespace.ObjectMeta, &oldNamespace.ObjectMeta, validation.NewFieldPath("metadata"))...) + + fldPath := validation.NewFieldPath("spec", "finalizers") for i := range newNamespace.Spec.Finalizers { - allErrs = append(allErrs, validateFinalizerName(string(newNamespace.Spec.Finalizers[i]))...) + idxPath := fldPath.Index(i) + allErrs = append(allErrs, validateFinalizerName(string(newNamespace.Spec.Finalizers[i]), idxPath)...) } newNamespace.Status = oldNamespace.Status return allErrs @@ -1919,85 +1952,82 @@ func ValidateNamespaceFinalizeUpdate(newNamespace, oldNamespace *api.Namespace) // ValidateEndpoints tests if required fields are set. func ValidateEndpoints(endpoints *api.Endpoints) validation.ErrorList { - allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMeta(&endpoints.ObjectMeta, true, ValidateEndpointsName).Prefix("metadata")...) - allErrs = append(allErrs, validateEndpointSubsets(endpoints.Subsets).Prefix("subsets")...) + allErrs := ValidateObjectMeta(&endpoints.ObjectMeta, true, ValidateEndpointsName, validation.NewFieldPath("metadata")) + allErrs = append(allErrs, validateEndpointSubsets(endpoints.Subsets, validation.NewFieldPath("subsets"))...) return allErrs } -func validateEndpointSubsets(subsets []api.EndpointSubset) validation.ErrorList { +func validateEndpointSubsets(subsets []api.EndpointSubset, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} for i := range subsets { ss := &subsets[i] - - ssErrs := validation.ErrorList{} + idxPath := fldPath.Index(i) if len(ss.Addresses) == 0 && len(ss.NotReadyAddresses) == 0 { - ssErrs = append(ssErrs, validation.NewRequiredError("addresses or notReadyAddresses")) + //TODO: consider adding a RequiredOneOf() error for this and similar cases + allErrs = append(allErrs, validation.NewRequiredError(idxPath.Child("addresses or notReadyAddresses"))) } if len(ss.Ports) == 0 { - ssErrs = append(ssErrs, validation.NewRequiredError("ports")) + allErrs = append(allErrs, validation.NewRequiredError(idxPath.Child("ports"))) } for addr := range ss.Addresses { - ssErrs = append(ssErrs, validateEndpointAddress(&ss.Addresses[addr]).PrefixIndex(addr).Prefix("addresses")...) + allErrs = append(allErrs, validateEndpointAddress(&ss.Addresses[addr], idxPath.Child("addresses").Index(addr))...) } for port := range ss.Ports { - ssErrs = append(ssErrs, validateEndpointPort(&ss.Ports[port], len(ss.Ports) > 1).PrefixIndex(port).Prefix("ports")...) + allErrs = append(allErrs, validateEndpointPort(&ss.Ports[port], len(ss.Ports) > 1, idxPath.Child("ports").Index(port))...) } - - allErrs = append(allErrs, ssErrs.PrefixIndex(i)...) } return allErrs } -func validateEndpointAddress(address *api.EndpointAddress) validation.ErrorList { +func validateEndpointAddress(address *api.EndpointAddress, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if !validation.IsValidIPv4(address.IP) { - allErrs = append(allErrs, validation.NewInvalidError("ip", address.IP, "invalid IPv4 address")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("ip"), address.IP, "invalid IPv4 address")) return allErrs } - return validateIpIsNotLinkLocalOrLoopback(address.IP, "ip") + return validateIpIsNotLinkLocalOrLoopback(address.IP, fldPath.Child("ip")) } -func validateIpIsNotLinkLocalOrLoopback(ipAddress, fieldName string) validation.ErrorList { +func validateIpIsNotLinkLocalOrLoopback(ipAddress string, fldPath *validation.FieldPath) validation.ErrorList { // We disallow some IPs as endpoints or external-ips. Specifically, loopback addresses are // nonsensical and link-local addresses tend to be used for node-centric purposes (e.g. metadata service). allErrs := validation.ErrorList{} ip := net.ParseIP(ipAddress) if ip == nil { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, ipAddress, "not a valid IP address")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, ipAddress, "not a valid IP address")) return allErrs } if ip.IsLoopback() { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, ipAddress, "may not be in the loopback range (127.0.0.0/8)")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, ipAddress, "may not be in the loopback range (127.0.0.0/8)")) } if ip.IsLinkLocalUnicast() { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, ipAddress, "may not be in the link-local range (169.254.0.0/16)")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, ipAddress, "may not be in the link-local range (169.254.0.0/16)")) } if ip.IsLinkLocalMulticast() { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, ipAddress, "may not be in the link-local multicast range (224.0.0.0/24)")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, ipAddress, "may not be in the link-local multicast range (224.0.0.0/24)")) } return allErrs } -func validateEndpointPort(port *api.EndpointPort, requireName bool) validation.ErrorList { +func validateEndpointPort(port *api.EndpointPort, requireName bool, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if requireName && port.Name == "" { - allErrs = append(allErrs, validation.NewRequiredError("name")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("name"))) } else if port.Name != "" { if !validation.IsDNS1123Label(port.Name) { - allErrs = append(allErrs, validation.NewInvalidError("name", port.Name, DNS1123LabelErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("name"), port.Name, DNS1123LabelErrorMsg)) } } if !validation.IsValidPortNum(port.Port) { - allErrs = append(allErrs, validation.NewInvalidError("port", port.Port, PortRangeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("port"), port.Port, PortRangeErrorMsg)) } if len(port.Protocol) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("protocol")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("protocol"))) } else if !supportedPortProtocols.Has(string(port.Protocol)) { - allErrs = append(allErrs, validation.NewNotSupportedError("protocol", port.Protocol, supportedPortProtocols.List())) + allErrs = append(allErrs, validation.NewNotSupportedError(fldPath.Child("protocol"), port.Protocol, supportedPortProtocols.List())) } return allErrs } @@ -2005,13 +2035,13 @@ func validateEndpointPort(port *api.EndpointPort, requireName bool) validation.E // ValidateEndpointsUpdate tests to make sure an endpoints update can be applied. func ValidateEndpointsUpdate(newEndpoints, oldEndpoints *api.Endpoints) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newEndpoints.ObjectMeta, &oldEndpoints.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, validateEndpointSubsets(newEndpoints.Subsets).Prefix("subsets")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&newEndpoints.ObjectMeta, &oldEndpoints.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, validateEndpointSubsets(newEndpoints.Subsets, validation.NewFieldPath("subsets"))...) return allErrs } // ValidateSecurityContext ensure the security context contains valid settings -func ValidateSecurityContext(sc *api.SecurityContext) validation.ErrorList { +func ValidateSecurityContext(sc *api.SecurityContext, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} //this should only be true for testing since SecurityContext is defaulted by the api if sc == nil { @@ -2020,13 +2050,13 @@ func ValidateSecurityContext(sc *api.SecurityContext) validation.ErrorList { if sc.Privileged != nil { if *sc.Privileged && !capabilities.Get().AllowPrivileged { - allErrs = append(allErrs, validation.NewForbiddenError("privileged", sc.Privileged)) + allErrs = append(allErrs, validation.NewForbiddenError(fldPath.Child("privileged"), sc.Privileged)) } } if sc.RunAsUser != nil { if *sc.RunAsUser < 0 { - allErrs = append(allErrs, validation.NewInvalidError("runAsUser", *sc.RunAsUser, "runAsUser cannot be negative")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("runAsUser"), *sc.RunAsUser, "runAsUser cannot be negative")) } } return allErrs @@ -2035,38 +2065,39 @@ func ValidateSecurityContext(sc *api.SecurityContext) validation.ErrorList { func ValidatePodLogOptions(opts *api.PodLogOptions) validation.ErrorList { allErrs := validation.ErrorList{} if opts.TailLines != nil && *opts.TailLines < 0 { - allErrs = append(allErrs, validation.NewInvalidError("tailLines", *opts.TailLines, "tailLines must be a non-negative integer or nil")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("tailLines"), *opts.TailLines, "tailLines must be a non-negative integer or nil")) } if opts.LimitBytes != nil && *opts.LimitBytes < 1 { - allErrs = append(allErrs, validation.NewInvalidError("limitBytes", *opts.LimitBytes, "limitBytes must be a positive integer or nil")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("limitBytes"), *opts.LimitBytes, "limitBytes must be a positive integer or nil")) } switch { case opts.SinceSeconds != nil && opts.SinceTime != nil: - allErrs = append(allErrs, validation.NewInvalidError("sinceSeconds", *opts.SinceSeconds, "only one of sinceTime or sinceSeconds can be provided")) - allErrs = append(allErrs, validation.NewInvalidError("sinceTime", *opts.SinceTime, "only one of sinceTime or sinceSeconds can be provided")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("sinceSeconds"), *opts.SinceSeconds, "only one of sinceTime or sinceSeconds can be provided")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("sinceTime"), *opts.SinceTime, "only one of sinceTime or sinceSeconds can be provided")) case opts.SinceSeconds != nil: if *opts.SinceSeconds < 1 { - allErrs = append(allErrs, validation.NewInvalidError("sinceSeconds", *opts.SinceSeconds, "sinceSeconds must be a positive integer")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("sinceSeconds"), *opts.SinceSeconds, "sinceSeconds must be a positive integer")) } } return allErrs } // ValidateLoadBalancerStatus validates required fields on a LoadBalancerStatus -func ValidateLoadBalancerStatus(status *api.LoadBalancerStatus) validation.ErrorList { +func ValidateLoadBalancerStatus(status *api.LoadBalancerStatus, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - for _, ingress := range status.Ingress { + for i, ingress := range status.Ingress { + idxPath := fldPath.Child("ingress").Index(i) if len(ingress.IP) > 0 { if isIP := (net.ParseIP(ingress.IP) != nil); !isIP { - allErrs = append(allErrs, validation.NewInvalidError("ingress.ip", ingress.IP, "must be an IP address")) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("ip"), ingress.IP, "must be an IP address")) } } if len(ingress.Hostname) > 0 { if valid, errMsg := NameIsDNSSubdomain(ingress.Hostname, false); !valid { - allErrs = append(allErrs, validation.NewInvalidError("ingress.hostname", ingress.Hostname, errMsg)) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("hostname"), ingress.Hostname, errMsg)) } if isIP := (net.ParseIP(ingress.Hostname) != nil); isIP { - allErrs = append(allErrs, validation.NewInvalidError("ingress.hostname", ingress.Hostname, "must be a DNS name, not an IP address")) + allErrs = append(allErrs, validation.NewInvalidError(idxPath.Child("hostname"), ingress.Hostname, "must be a DNS name, not an IP address")) } } } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 3419ae80e7a..9f00a184ad1 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -43,12 +43,16 @@ func expectPrefix(t *testing.T, prefix string, errs validation.ErrorList) { // Ensure custom name functions are allowed func TestValidateObjectMetaCustomName(t *testing.T) { - errs := ValidateObjectMeta(&api.ObjectMeta{Name: "test", GenerateName: "foo"}, false, func(s string, prefix bool) (bool, string) { - if s == "test" { - return true, "" - } - return false, "name-gen" - }) + errs := ValidateObjectMeta( + &api.ObjectMeta{Name: "test", GenerateName: "foo"}, + false, + func(s string, prefix bool) (bool, string) { + if s == "test" { + return true, "" + } + return false, "name-gen" + }, + validation.NewFieldPath("field")) if len(errs) != 1 { t.Fatalf("unexpected errors: %v", errs) } @@ -59,9 +63,13 @@ func TestValidateObjectMetaCustomName(t *testing.T) { // Ensure namespace names follow dns label format func TestValidateObjectMetaNamespaces(t *testing.T) { - errs := ValidateObjectMeta(&api.ObjectMeta{Name: "test", Namespace: "foo.bar"}, false, func(s string, prefix bool) (bool, string) { - return true, "" - }) + errs := ValidateObjectMeta( + &api.ObjectMeta{Name: "test", Namespace: "foo.bar"}, + false, + func(s string, prefix bool) (bool, string) { + return true, "" + }, + validation.NewFieldPath("field")) if len(errs) != 1 { t.Fatalf("unexpected errors: %v", errs) } @@ -74,9 +82,13 @@ func TestValidateObjectMetaNamespaces(t *testing.T) { for i := range b { b[i] = letters[rand.Intn(len(letters))] } - errs = ValidateObjectMeta(&api.ObjectMeta{Name: "test", Namespace: string(b)}, false, func(s string, prefix bool) (bool, string) { - return true, "" - }) + errs = ValidateObjectMeta( + &api.ObjectMeta{Name: "test", Namespace: string(b)}, + false, + func(s string, prefix bool) (bool, string) { + return true, "" + }, + validation.NewFieldPath("field")) if len(errs) != 1 { t.Fatalf("unexpected errors: %v", errs) } @@ -89,18 +101,21 @@ func TestValidateObjectMetaUpdateIgnoresCreationTimestamp(t *testing.T) { if errs := ValidateObjectMetaUpdate( &api.ObjectMeta{Name: "test", ResourceVersion: "1"}, &api.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: unversioned.NewTime(time.Unix(10, 0))}, + validation.NewFieldPath("field"), ); len(errs) != 0 { t.Fatalf("unexpected errors: %v", errs) } if errs := ValidateObjectMetaUpdate( &api.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: unversioned.NewTime(time.Unix(10, 0))}, &api.ObjectMeta{Name: "test", ResourceVersion: "1"}, + validation.NewFieldPath("field"), ); len(errs) != 0 { t.Fatalf("unexpected errors: %v", errs) } if errs := ValidateObjectMetaUpdate( &api.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: unversioned.NewTime(time.Unix(10, 0))}, &api.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: unversioned.NewTime(time.Unix(11, 0))}, + validation.NewFieldPath("field"), ); len(errs) != 0 { t.Fatalf("unexpected errors: %v", errs) } @@ -108,7 +123,11 @@ func TestValidateObjectMetaUpdateIgnoresCreationTimestamp(t *testing.T) { // Ensure trailing slash is allowed in generate name func TestValidateObjectMetaTrimsTrailingSlash(t *testing.T) { - errs := ValidateObjectMeta(&api.ObjectMeta{Name: "test", GenerateName: "foo-"}, false, NameIsDNSSubdomain) + errs := ValidateObjectMeta( + &api.ObjectMeta{Name: "test", GenerateName: "foo-"}, + false, + NameIsDNSSubdomain, + validation.NewFieldPath("field")) if len(errs) != 0 { t.Fatalf("unexpected errors: %v", errs) } @@ -132,7 +151,7 @@ func TestValidateLabels(t *testing.T) { {"goodvalue": "123_-.BaR"}, } for i := range successCases { - errs := ValidateLabels(successCases[i], "field") + errs := ValidateLabels(successCases[i], validation.NewFieldPath("field")) if len(errs) != 0 { t.Errorf("case[%d] expected success, got %#v", i, errs) } @@ -145,7 +164,7 @@ func TestValidateLabels(t *testing.T) { {strings.Repeat("a", 254): "bar"}, } for i := range labelNameErrorCases { - errs := ValidateLabels(labelNameErrorCases[i], "field") + errs := ValidateLabels(labelNameErrorCases[i], validation.NewFieldPath("field")) if len(errs) != 1 { t.Errorf("case[%d] expected failure", i) } else { @@ -163,7 +182,7 @@ func TestValidateLabels(t *testing.T) { {"strangecharsinvalue": "?#$notsogood"}, } for i := range labelValueErrorCases { - errs := ValidateLabels(labelValueErrorCases[i], "field") + errs := ValidateLabels(labelValueErrorCases[i], validation.NewFieldPath("field")) if len(errs) != 1 { t.Errorf("case[%d] expected failure", i) } else { @@ -197,7 +216,7 @@ func TestValidateAnnotations(t *testing.T) { }, } for i := range successCases { - errs := ValidateAnnotations(successCases[i], "field") + errs := ValidateAnnotations(successCases[i], validation.NewFieldPath("field")) if len(errs) != 0 { t.Errorf("case[%d] expected success, got %#v", i, errs) } @@ -210,7 +229,7 @@ func TestValidateAnnotations(t *testing.T) { {strings.Repeat("a", 254): "bar"}, } for i := range nameErrorCases { - errs := ValidateAnnotations(nameErrorCases[i], "field") + errs := ValidateAnnotations(nameErrorCases[i], validation.NewFieldPath("field")) if len(errs) != 1 { t.Errorf("case[%d] expected failure", i) } @@ -227,7 +246,7 @@ func TestValidateAnnotations(t *testing.T) { }, } for i := range totalSizeErrorCases { - errs := ValidateAnnotations(totalSizeErrorCases[i], "field") + errs := ValidateAnnotations(totalSizeErrorCases[i], validation.NewFieldPath("field")) if len(errs) != 1 { t.Errorf("case[%d] expected failure", i) } @@ -490,7 +509,7 @@ func TestValidateVolumes(t *testing.T) { }}}}, {Name: "fc", VolumeSource: api.VolumeSource{FC: &api.FCVolumeSource{[]string{"some_wwn"}, &lun, "ext4", false}}}, } - names, errs := validateVolumes(successCase) + names, errs := validateVolumes(successCase, validation.NewFieldPath("field")) if len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -543,46 +562,137 @@ func TestValidateVolumes(t *testing.T) { F string D string }{ - "zero-length name": {[]api.Volume{{Name: "", VolumeSource: emptyVS}}, validation.ErrorTypeRequired, "[0].name", ""}, - "name > 63 characters": {[]api.Volume{{Name: strings.Repeat("a", 64), VolumeSource: emptyVS}}, validation.ErrorTypeInvalid, "[0].name", "must be a DNS label (at most 63 characters, matching regex [a-z0-9]([-a-z0-9]*[a-z0-9])?): e.g. \"my-name\""}, - "name not a DNS label": {[]api.Volume{{Name: "a.b.c", VolumeSource: emptyVS}}, validation.ErrorTypeInvalid, "[0].name", "must be a DNS label (at most 63 characters, matching regex [a-z0-9]([-a-z0-9]*[a-z0-9])?): e.g. \"my-name\""}, - "name not unique": {[]api.Volume{{Name: "abc", VolumeSource: emptyVS}, {Name: "abc", VolumeSource: emptyVS}}, validation.ErrorTypeDuplicate, "[1].name", ""}, - "empty portal": {[]api.Volume{{Name: "badportal", VolumeSource: emptyPortal}}, validation.ErrorTypeRequired, "[0].source.iscsi.targetPortal", ""}, - "empty iqn": {[]api.Volume{{Name: "badiqn", VolumeSource: emptyIQN}}, validation.ErrorTypeRequired, "[0].source.iscsi.iqn", ""}, - "empty hosts": {[]api.Volume{{Name: "badhost", VolumeSource: emptyHosts}}, validation.ErrorTypeRequired, "[0].source.glusterfs.endpoints", ""}, - "empty path": {[]api.Volume{{Name: "badpath", VolumeSource: emptyPath}}, validation.ErrorTypeRequired, "[0].source.glusterfs.path", ""}, - "empty datasetName": {[]api.Volume{{Name: "badname", VolumeSource: emptyName}}, validation.ErrorTypeRequired, "[0].source.flocker.datasetName", ""}, - "empty mon": {[]api.Volume{{Name: "badmon", VolumeSource: emptyMon}}, validation.ErrorTypeRequired, "[0].source.rbd.monitors", ""}, - "empty image": {[]api.Volume{{Name: "badimage", VolumeSource: emptyImage}}, validation.ErrorTypeRequired, "[0].source.rbd.image", ""}, - "empty cephfs mon": {[]api.Volume{{Name: "badmon", VolumeSource: emptyCephFSMon}}, validation.ErrorTypeRequired, "[0].source.cephfs.monitors", ""}, - "empty metatada path": {[]api.Volume{{Name: "emptyname", VolumeSource: emptyPathName}}, validation.ErrorTypeRequired, "[0].source.downwardApi.path", ""}, - "absolute path": {[]api.Volume{{Name: "absolutepath", VolumeSource: absolutePathName}}, validation.ErrorTypeForbidden, "[0].source.downwardApi.path", ""}, - "dot dot path": {[]api.Volume{{Name: "dotdotpath", VolumeSource: dotDotInPath}}, validation.ErrorTypeInvalid, "[0].source.downwardApi.path", "must not contain \"..\""}, - "dot dot file name": {[]api.Volume{{Name: "dotdotfilename", VolumeSource: dotDotPathName}}, validation.ErrorTypeInvalid, "[0].source.downwardApi.path", "must not start with \"..\""}, - "dot dot first level dirent": {[]api.Volume{{Name: "dotdotdirfilename", VolumeSource: dotDotFirstLevelDirent}}, validation.ErrorTypeInvalid, "[0].source.downwardApi.path", "must not start with \"..\""}, - "empty wwn": {[]api.Volume{{Name: "badimage", VolumeSource: zeroWWN}}, validation.ErrorTypeRequired, "[0].source.fc.targetWWNs", ""}, - "empty lun": {[]api.Volume{{Name: "badimage", VolumeSource: emptyLun}}, validation.ErrorTypeRequired, "[0].source.fc.lun", ""}, - "slash in datasetName": {[]api.Volume{{Name: "slashinname", VolumeSource: slashInName}}, validation.ErrorTypeInvalid, "[0].source.flocker.datasetName", "must not contain '/'"}, - "starts with '..'": {[]api.Volume{{Name: "badprefix", VolumeSource: startsWithDots}}, validation.ErrorTypeInvalid, "[0].source.gitRepo.directory", "must not start with \"..\""}, - "contains '..'": {[]api.Volume{{Name: "containsdots", VolumeSource: containsDots}}, validation.ErrorTypeInvalid, "[0].source.gitRepo.directory", "must not contain \"..\""}, - "absolute target": {[]api.Volume{{Name: "absolutetarget", VolumeSource: absPath}}, validation.ErrorTypeForbidden, "[0].source.gitRepo.directory", ""}, + "zero-length name": { + []api.Volume{{Name: "", VolumeSource: emptyVS}}, + validation.ErrorTypeRequired, + "name", "", + }, + "name > 63 characters": { + []api.Volume{{Name: strings.Repeat("a", 64), VolumeSource: emptyVS}}, + validation.ErrorTypeInvalid, + "name", "must be a DNS label", + }, + "name not a DNS label": { + []api.Volume{{Name: "a.b.c", VolumeSource: emptyVS}}, + validation.ErrorTypeInvalid, + "name", "must be a DNS label", + }, + "name not unique": { + []api.Volume{{Name: "abc", VolumeSource: emptyVS}, {Name: "abc", VolumeSource: emptyVS}}, + validation.ErrorTypeDuplicate, + "[1].name", "", + }, + "empty portal": { + []api.Volume{{Name: "badportal", VolumeSource: emptyPortal}}, + validation.ErrorTypeRequired, + "iscsi.targetPortal", "", + }, + "empty iqn": { + []api.Volume{{Name: "badiqn", VolumeSource: emptyIQN}}, + validation.ErrorTypeRequired, + "iscsi.iqn", "", + }, + "empty hosts": { + []api.Volume{{Name: "badhost", VolumeSource: emptyHosts}}, + validation.ErrorTypeRequired, + "glusterfs.endpoints", "", + }, + "empty path": { + []api.Volume{{Name: "badpath", VolumeSource: emptyPath}}, + validation.ErrorTypeRequired, + "glusterfs.path", "", + }, + "empty datasetName": { + []api.Volume{{Name: "badname", VolumeSource: emptyName}}, + validation.ErrorTypeRequired, + "flocker.datasetName", "", + }, + "empty mon": { + []api.Volume{{Name: "badmon", VolumeSource: emptyMon}}, + validation.ErrorTypeRequired, + "rbd.monitors", "", + }, + "empty image": { + []api.Volume{{Name: "badimage", VolumeSource: emptyImage}}, + validation.ErrorTypeRequired, + "rbd.image", "", + }, + "empty cephfs mon": { + []api.Volume{{Name: "badmon", VolumeSource: emptyCephFSMon}}, + validation.ErrorTypeRequired, + "cephfs.monitors", "", + }, + "empty metatada path": { + []api.Volume{{Name: "emptyname", VolumeSource: emptyPathName}}, + validation.ErrorTypeRequired, + "downwardAPI.path", "", + }, + "absolute path": { + []api.Volume{{Name: "absolutepath", VolumeSource: absolutePathName}}, + validation.ErrorTypeForbidden, + "downwardAPI.path", "", + }, + "dot dot path": { + []api.Volume{{Name: "dotdotpath", VolumeSource: dotDotInPath}}, + validation.ErrorTypeInvalid, + "downwardAPI.path", `must not contain ".."`, + }, + "dot dot file name": { + []api.Volume{{Name: "dotdotfilename", VolumeSource: dotDotPathName}}, + validation.ErrorTypeInvalid, + "downwardAPI.path", `must not start with ".."`, + }, + "dot dot first level dirent": { + []api.Volume{{Name: "dotdotdirfilename", VolumeSource: dotDotFirstLevelDirent}}, + validation.ErrorTypeInvalid, + "downwardAPI.path", `must not start with ".."`, + }, + "empty wwn": { + []api.Volume{{Name: "badimage", VolumeSource: zeroWWN}}, + validation.ErrorTypeRequired, + "fc.targetWWNs", "", + }, + "empty lun": { + []api.Volume{{Name: "badimage", VolumeSource: emptyLun}}, + validation.ErrorTypeRequired, + "fc.lun", "", + }, + "slash in datasetName": { + []api.Volume{{Name: "slashinname", VolumeSource: slashInName}}, + validation.ErrorTypeInvalid, + "flocker.datasetName", "must not contain '/'", + }, + "starts with '..'": { + []api.Volume{{Name: "badprefix", VolumeSource: startsWithDots}}, + validation.ErrorTypeInvalid, + "gitRepo.directory", `must not start with ".."`, + }, + "contains '..'": { + []api.Volume{{Name: "containsdots", VolumeSource: containsDots}}, + validation.ErrorTypeInvalid, + "gitRepo.directory", `must not contain ".."`, + }, + "absolute target": { + []api.Volume{{Name: "absolutetarget", VolumeSource: absPath}}, + validation.ErrorTypeForbidden, + "gitRepo.directory", "", + }, } for k, v := range errorCases { - _, errs := validateVolumes(v.V) + _, errs := validateVolumes(v.V, validation.NewFieldPath("field")) if len(errs) == 0 { t.Errorf("expected failure %s for %v", k, v.V) continue } for i := range errs { if errs[i].Type != v.T { - t.Errorf("%s: expected errors to have type %s: %v", k, v.T, errs[i]) + t.Errorf("%s: expected error to have type %q: %q", k, v.T, errs[i].Type) } - if errs[i].Field != v.F { - t.Errorf("%s: expected errors to have field %s: %v", k, v.F, errs[i]) + if !strings.Contains(errs[i].Field, v.F) { + t.Errorf("%s: expected error field %q: %q", k, v.F, errs[i].Field) } - detail := errs[i].Detail - if detail != v.D { - t.Errorf("%s: expected error detail \"%s\", got \"%s\"", k, v.D, detail) + if !strings.Contains(errs[i].Detail, v.D) { + t.Errorf("%s: expected error detail %q, got %q", k, v.D, errs[i].Detail) } } } @@ -596,14 +706,14 @@ func TestValidatePorts(t *testing.T) { {Name: "do-re-me", ContainerPort: 84, Protocol: "UDP"}, {ContainerPort: 85, Protocol: "TCP"}, } - if errs := validatePorts(successCase); len(errs) != 0 { + if errs := validateContainerPorts(successCase, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } nonCanonicalCase := []api.ContainerPort{ {ContainerPort: 80, Protocol: "TCP"}, } - if errs := validatePorts(nonCanonicalCase); len(errs) != 0 { + if errs := validateContainerPorts(nonCanonicalCase, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -613,35 +723,74 @@ func TestValidatePorts(t *testing.T) { F string D string }{ - "name > 15 characters": {[]api.ContainerPort{{Name: strings.Repeat("a", 16), ContainerPort: 80, Protocol: "TCP"}}, validation.ErrorTypeInvalid, "[0].name", PortNameErrorMsg}, - "name not a IANA svc name ": {[]api.ContainerPort{{Name: "a.b.c", ContainerPort: 80, Protocol: "TCP"}}, validation.ErrorTypeInvalid, "[0].name", PortNameErrorMsg}, - "name not a IANA svc name (i.e. a number)": {[]api.ContainerPort{{Name: "80", ContainerPort: 80, Protocol: "TCP"}}, validation.ErrorTypeInvalid, "[0].name", PortNameErrorMsg}, - "name not unique": {[]api.ContainerPort{ - {Name: "abc", ContainerPort: 80, Protocol: "TCP"}, - {Name: "abc", ContainerPort: 81, Protocol: "TCP"}, - }, validation.ErrorTypeDuplicate, "[1].name", ""}, - "zero container port": {[]api.ContainerPort{{ContainerPort: 0, Protocol: "TCP"}}, validation.ErrorTypeInvalid, "[0].containerPort", PortRangeErrorMsg}, - "invalid container port": {[]api.ContainerPort{{ContainerPort: 65536, Protocol: "TCP"}}, validation.ErrorTypeInvalid, "[0].containerPort", PortRangeErrorMsg}, - "invalid host port": {[]api.ContainerPort{{ContainerPort: 80, HostPort: 65536, Protocol: "TCP"}}, validation.ErrorTypeInvalid, "[0].hostPort", PortRangeErrorMsg}, - "invalid protocol case": {[]api.ContainerPort{{ContainerPort: 80, Protocol: "tcp"}}, validation.ErrorTypeNotSupported, "[0].protocol", "supported values: TCP, UDP"}, - "invalid protocol": {[]api.ContainerPort{{ContainerPort: 80, Protocol: "ICMP"}}, validation.ErrorTypeNotSupported, "[0].protocol", "supported values: TCP, UDP"}, - "protocol required": {[]api.ContainerPort{{Name: "abc", ContainerPort: 80}}, validation.ErrorTypeRequired, "[0].protocol", ""}, + "name > 15 characters": { + []api.ContainerPort{{Name: strings.Repeat("a", 16), ContainerPort: 80, Protocol: "TCP"}}, + validation.ErrorTypeInvalid, + "name", PortNameErrorMsg, + }, + "name not a IANA svc name ": { + []api.ContainerPort{{Name: "a.b.c", ContainerPort: 80, Protocol: "TCP"}}, + validation.ErrorTypeInvalid, + "name", PortNameErrorMsg, + }, + "name not a IANA svc name (i.e. a number)": { + []api.ContainerPort{{Name: "80", ContainerPort: 80, Protocol: "TCP"}}, + validation.ErrorTypeInvalid, + "name", PortNameErrorMsg, + }, + "name not unique": { + []api.ContainerPort{ + {Name: "abc", ContainerPort: 80, Protocol: "TCP"}, + {Name: "abc", ContainerPort: 81, Protocol: "TCP"}, + }, + validation.ErrorTypeDuplicate, + "[1].name", "", + }, + "zero container port": { + []api.ContainerPort{{ContainerPort: 0, Protocol: "TCP"}}, + validation.ErrorTypeInvalid, + "containerPort", PortRangeErrorMsg, + }, + "invalid container port": { + []api.ContainerPort{{ContainerPort: 65536, Protocol: "TCP"}}, + validation.ErrorTypeInvalid, + "containerPort", PortRangeErrorMsg, + }, + "invalid host port": { + []api.ContainerPort{{ContainerPort: 80, HostPort: 65536, Protocol: "TCP"}}, + validation.ErrorTypeInvalid, + "hostPort", PortRangeErrorMsg, + }, + "invalid protocol case": { + []api.ContainerPort{{ContainerPort: 80, Protocol: "tcp"}}, + validation.ErrorTypeNotSupported, + "protocol", "supported values: TCP, UDP", + }, + "invalid protocol": { + []api.ContainerPort{{ContainerPort: 80, Protocol: "ICMP"}}, + validation.ErrorTypeNotSupported, + "protocol", "supported values: TCP, UDP", + }, + "protocol required": { + []api.ContainerPort{{Name: "abc", ContainerPort: 80}}, + validation.ErrorTypeRequired, + "protocol", "", + }, } for k, v := range errorCases { - errs := validatePorts(v.P) + errs := validateContainerPorts(v.P, validation.NewFieldPath("field")) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } for i := range errs { if errs[i].Type != v.T { - t.Errorf("%s: expected errors to have type %s: %v", k, v.T, errs[i]) + t.Errorf("%s: expected error to have type %q: %q", k, v.T, errs[i].Type) } - if errs[i].Field != v.F { - t.Errorf("%s: expected errors to have field %s: %v", k, v.F, errs[i]) + if !strings.Contains(errs[i].Field, v.F) { + t.Errorf("%s: expected error field %q: %q", k, v.F, errs[i].Field) } - detail := errs[i].Detail - if detail != v.D { - t.Errorf("%s: expected error detail either empty or %s, got %s", k, v.D, detail) + if !strings.Contains(errs[i].Detail, v.D) { + t.Errorf("%s: expected error detail %q, got %q", k, v.D, errs[i].Detail) } } } @@ -663,7 +812,7 @@ func TestValidateEnv(t *testing.T) { }, }, } - if errs := validateEnv(successCase); len(errs) != 0 { + if errs := validateEnv(successCase, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -770,17 +919,17 @@ func TestValidateEnv(t *testing.T) { }, }, }}, - expectedError: "[0].valueFrom.fieldRef.fieldPath: unsupported value 'status.phase', Details: supported values: metadata.name, metadata.namespace, status.podIP", + expectedError: "valueFrom.fieldRef.fieldPath: unsupported value 'status.phase', Details: supported values: metadata.name, metadata.namespace, status.podIP", }, } for _, tc := range errorCases { - if errs := validateEnv(tc.envs); len(errs) == 0 { + if errs := validateEnv(tc.envs, validation.NewFieldPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", tc.name) } else { for i := range errs { str := errs[i].Error() - if str != "" && str != tc.expectedError { - t.Errorf("%s: expected error detail either empty or %s, got %s", tc.name, tc.expectedError, str) + if str != "" && !strings.Contains(str, tc.expectedError) { + t.Errorf("%s: expected error detail either empty or %q, got %q", tc.name, tc.expectedError, str) } } } @@ -795,7 +944,7 @@ func TestValidateVolumeMounts(t *testing.T) { {Name: "123", MountPath: "/foo"}, {Name: "abc-123", MountPath: "/bar"}, } - if errs := validateVolumeMounts(successCase, volumes); len(errs) != 0 { + if errs := validateVolumeMounts(successCase, volumes, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -805,7 +954,7 @@ func TestValidateVolumeMounts(t *testing.T) { "empty mountpath": {{Name: "abc", MountPath: ""}}, } for k, v := range errorCases { - if errs := validateVolumeMounts(v, volumes); len(errs) == 0 { + if errs := validateVolumeMounts(v, volumes, validation.NewFieldPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", k) } } @@ -823,7 +972,7 @@ func TestValidateProbe(t *testing.T) { } for _, p := range successCases { - if errs := validateProbe(p); len(errs) != 0 { + if errs := validateProbe(p, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -835,7 +984,7 @@ func TestValidateProbe(t *testing.T) { errorCases = append(errorCases, probe) } for _, p := range errorCases { - if errs := validateProbe(p); len(errs) == 0 { + if errs := validateProbe(p, validation.NewFieldPath("field")); len(errs) == 0 { t.Errorf("expected failure for %v", p) } } @@ -849,7 +998,7 @@ func TestValidateHandler(t *testing.T) { {HTTPGet: &api.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP"}}, } for _, h := range successCases { - if errs := validateHandler(&h); len(errs) != 0 { + if errs := validateHandler(&h, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -862,7 +1011,7 @@ func TestValidateHandler(t *testing.T) { {HTTPGet: &api.HTTPGetAction{Path: "", Port: intstr.FromString(""), Host: ""}}, } for _, h := range errorCases { - if errs := validateHandler(&h); len(errs) == 0 { + if errs := validateHandler(&h, validation.NewFieldPath("field")); len(errs) == 0 { t.Errorf("expected failure for %#v", h) } } @@ -901,7 +1050,7 @@ func TestValidatePullPolicy(t *testing.T) { } for k, v := range testCases { ctr := &v.Container - errs := validatePullPolicy(ctr) + errs := validatePullPolicy(ctr.ImagePullPolicy, validation.NewFieldPath("field")) if len(errs) != 0 { t.Errorf("case[%s] expected success, got %#v", k, errs) } @@ -1017,7 +1166,7 @@ func TestValidateContainers(t *testing.T) { }, {Name: "abc-1234", Image: "image", ImagePullPolicy: "IfNotPresent", SecurityContext: fakeValidSecurityContext(true)}, } - if errs := validateContainers(successCase, volumes); len(errs) != 0 { + if errs := validateContainers(successCase, volumes, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -1197,7 +1346,7 @@ func TestValidateContainers(t *testing.T) { }, } for k, v := range errorCases { - if errs := validateContainers(v, volumes); len(errs) == 0 { + if errs := validateContainers(v, volumes, validation.NewFieldPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", k) } } @@ -1210,7 +1359,7 @@ func TestValidateRestartPolicy(t *testing.T) { api.RestartPolicyNever, } for _, policy := range successCases { - if errs := validateRestartPolicy(&policy); len(errs) != 0 { + if errs := validateRestartPolicy(&policy, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -1218,7 +1367,7 @@ func TestValidateRestartPolicy(t *testing.T) { errorCases := []api.RestartPolicy{"", "newpolicy"} for k, policy := range errorCases { - if errs := validateRestartPolicy(&policy); len(errs) == 0 { + if errs := validateRestartPolicy(&policy, validation.NewFieldPath("field")); len(errs) == 0 { t.Errorf("expected failure for %d", k) } } @@ -1227,14 +1376,14 @@ func TestValidateRestartPolicy(t *testing.T) { func TestValidateDNSPolicy(t *testing.T) { successCases := []api.DNSPolicy{api.DNSClusterFirst, api.DNSDefault, api.DNSPolicy(api.DNSClusterFirst)} for _, policy := range successCases { - if errs := validateDNSPolicy(&policy); len(errs) != 0 { + if errs := validateDNSPolicy(&policy, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } errorCases := []api.DNSPolicy{api.DNSPolicy("invalid")} for _, policy := range errorCases { - if errs := validateDNSPolicy(&policy); len(errs) == 0 { + if errs := validateDNSPolicy(&policy, validation.NewFieldPath("field")); len(errs) == 0 { t.Errorf("expected failure for %v", policy) } } @@ -1295,7 +1444,7 @@ func TestValidatePodSpec(t *testing.T) { }, } for i := range successCases { - if errs := ValidatePodSpec(&successCases[i]); len(errs) != 0 { + if errs := ValidatePodSpec(&successCases[i], validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -1360,7 +1509,7 @@ func TestValidatePodSpec(t *testing.T) { }, } for k, v := range failureCases { - if errs := ValidatePodSpec(&v); len(errs) == 0 { + if errs := ValidatePodSpec(&v, validation.NewFieldPath("field")); len(errs) == 0 { t.Errorf("expected failure for %q", k) } } @@ -2715,7 +2864,7 @@ func TestValidateNode(t *testing.T) { "metadata.labels": true, "metadata.annotations": true, "metadata.namespace": true, - "spec.ExternalID": true, + "spec.externalID": true, } if expectedFields[field] == false { t.Errorf("%s: missing prefix for: %v", k, errs[i]) @@ -3035,7 +3184,7 @@ func TestValidateResourceNames(t *testing.T) { {"kubernetes.io/will/not/work/", false}, } for k, item := range table { - err := validateResourceName(item.input, "sth") + err := validateResourceName(item.input, validation.NewFieldPath("field")) if len(err) != 0 && item.success { t.Errorf("expected no failure for input %q", item.input) } else if len(err) == 0 && !item.success { @@ -3158,7 +3307,7 @@ func TestValidateLimitRange(t *testing.T) { }, }, }}, - "Default is not supported when limit type is Pod", + "not supported when limit type is Pod", }, "default-request-limit-type-pod": { api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{ @@ -3171,7 +3320,7 @@ func TestValidateLimitRange(t *testing.T) { }, }, }}, - "DefaultRequest is not supported when limit type is Pod", + "not supported when limit type is Pod", }, "min value 100m is greater than max value 10m": { api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{ @@ -3234,7 +3383,7 @@ func TestValidateLimitRange(t *testing.T) { }, }, }}, - "maxLimitRequestRatio 800m is less than 1", + "ratio 800m is less than 1", }, "invalid spec maxLimitRequestRatio greater than max/min": { api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{ @@ -3247,7 +3396,7 @@ func TestValidateLimitRange(t *testing.T) { }, }, }}, - "maxLimitRequestRatio 10 is greater than max/min = 4.000000", + "ratio 10 is greater than max/min = 4.000000", }, } @@ -3259,7 +3408,7 @@ func TestValidateLimitRange(t *testing.T) { for i := range errs { detail := errs[i].Detail if detail != v.D { - t.Errorf("%s: expected error detail either empty or %s, got %s", k, v.D, detail) + t.Errorf("[%s]: expected error detail either empty or %q, got %q", k, v.D, detail) } } } @@ -3362,13 +3511,8 @@ func TestValidateResourceQuota(t *testing.T) { t.Errorf("expected failure for %s", k) } for i := range errs { - field := errs[i].Field - detail := errs[i].Detail - if field != "metadata.name" && field != "metadata.namespace" && !api.IsStandardResourceName(field) { - t.Errorf("%s: missing prefix for: %v", k, field) - } - if detail != v.D { - t.Errorf("%s: expected error detail either empty or %s, got %s", k, v.D, detail) + if errs[i].Detail != v.D { + t.Errorf("[%s]: expected error detail either empty or %s, got %s", k, v.D, errs[i].Detail) } } } @@ -4020,7 +4164,7 @@ func TestValidateSecurityContext(t *testing.T) { "no run as user": {noRunAsUser}, } for k, v := range successCases { - if errs := ValidateSecurityContext(v.sc); len(errs) != 0 { + if errs := ValidateSecurityContext(v.sc, validation.NewFieldPath("field")); len(errs) != 0 { t.Errorf("Expected success for %s, got %v", k, errs) } } @@ -4050,7 +4194,7 @@ func TestValidateSecurityContext(t *testing.T) { }, } for k, v := range errorCases { - if errs := ValidateSecurityContext(v.sc); len(errs) == 0 || errs[0].Type != v.errorType || errs[0].Detail != v.errorDetail { + if errs := ValidateSecurityContext(v.sc, validation.NewFieldPath("field")); len(errs) == 0 || errs[0].Type != v.errorType || errs[0].Detail != v.errorDetail { t.Errorf("Expected error type %s with detail %s for %s, got %v", v.errorType, v.errorDetail, k, errs) } } diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index 932aa7ef122..33820863425 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -17,7 +17,6 @@ limitations under the License. package validation import ( - "fmt" "net" "regexp" "strconv" @@ -39,77 +38,77 @@ func ValidateHorizontalPodAutoscalerName(name string, prefix bool) (bool, string return apivalidation.ValidateReplicationControllerName(name, prefix) } -func validateHorizontalPodAutoscalerSpec(autoscaler extensions.HorizontalPodAutoscalerSpec) validation.ErrorList { +func validateHorizontalPodAutoscalerSpec(autoscaler extensions.HorizontalPodAutoscalerSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if autoscaler.MinReplicas != nil && *autoscaler.MinReplicas < 1 { - allErrs = append(allErrs, validation.NewInvalidError("minReplicas", autoscaler.MinReplicas, `must be bigger or equal to 1`)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("minReplicas"), autoscaler.MinReplicas, `must be greater than or equal to 1`)) } if autoscaler.MaxReplicas < 1 { - allErrs = append(allErrs, validation.NewInvalidError("maxReplicas", autoscaler.MaxReplicas, `must be bigger or equal to 1`)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, `must be greater than or equal to 1`)) } if autoscaler.MinReplicas != nil && autoscaler.MaxReplicas < *autoscaler.MinReplicas { - allErrs = append(allErrs, validation.NewInvalidError("maxReplicas", autoscaler.MaxReplicas, `must be bigger or equal to minReplicas`)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, `must be greater than or equal to minReplicas`)) } if autoscaler.CPUUtilization != nil && autoscaler.CPUUtilization.TargetPercentage < 1 { - allErrs = append(allErrs, validation.NewInvalidError("cpuUtilization.targetPercentage", autoscaler.CPUUtilization.TargetPercentage, `must be bigger or equal to 1`)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("cpuUtilization", "targetPercentage"), autoscaler.CPUUtilization.TargetPercentage, `must be greater than or equal to 1`)) } - if refErrs := ValidateSubresourceReference(autoscaler.ScaleRef); len(refErrs) > 0 { + if refErrs := ValidateSubresourceReference(autoscaler.ScaleRef, fldPath.Child("scaleRef")); len(refErrs) > 0 { allErrs = append(allErrs, refErrs.Prefix("scaleRef")...) } else if autoscaler.ScaleRef.Subresource != "scale" { - allErrs = append(allErrs, validation.NewNotSupportedError("scaleRef.subresource", autoscaler.ScaleRef.Subresource, []string{"scale"})) + allErrs = append(allErrs, validation.NewNotSupportedError(fldPath.Child("scaleRef", "subresource"), autoscaler.ScaleRef.Subresource, []string{"scale"})) } return allErrs } -func ValidateSubresourceReference(ref extensions.SubresourceReference) validation.ErrorList { +func ValidateSubresourceReference(ref extensions.SubresourceReference, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(ref.Kind) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("kind")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("kind"))) } else if ok, msg := apivalidation.IsValidPathSegmentName(ref.Kind); !ok { - allErrs = append(allErrs, validation.NewInvalidError("kind", ref.Kind, msg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("kind"), ref.Kind, msg)) } if len(ref.Name) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("name")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("name"))) } else if ok, msg := apivalidation.IsValidPathSegmentName(ref.Name); !ok { - allErrs = append(allErrs, validation.NewInvalidError("name", ref.Name, msg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("name"), ref.Name, msg)) } if len(ref.Subresource) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("subresource")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("subresource"))) } else if ok, msg := apivalidation.IsValidPathSegmentName(ref.Subresource); !ok { - allErrs = append(allErrs, validation.NewInvalidError("subresource", ref.Subresource, msg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("subresource"), ref.Subresource, msg)) } return allErrs } func ValidateHorizontalPodAutoscaler(autoscaler *extensions.HorizontalPodAutoscaler) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&autoscaler.ObjectMeta, true, ValidateHorizontalPodAutoscalerName).Prefix("metadata")...) - allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(autoscaler.Spec)...) + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&autoscaler.ObjectMeta, true, ValidateHorizontalPodAutoscalerName, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(autoscaler.Spec, validation.NewFieldPath("spec"))...) return allErrs } -func ValidateHorizontalPodAutoscalerUpdate(newAutoscler, oldAutoscaler *extensions.HorizontalPodAutoscaler) validation.ErrorList { +func ValidateHorizontalPodAutoscalerUpdate(newAutoscaler, oldAutoscaler *extensions.HorizontalPodAutoscaler) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&newAutoscler.ObjectMeta, &oldAutoscaler.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(newAutoscler.Spec)...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&newAutoscaler.ObjectMeta, &oldAutoscaler.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(newAutoscaler.Spec, validation.NewFieldPath("spec"))...) return allErrs } func ValidateHorizontalPodAutoscalerStatusUpdate(controller, oldController *extensions.HorizontalPodAutoscaler) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta, validation.NewFieldPath("metadata"))...) status := controller.Status - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.CurrentReplicas), "currentReplicas")...) - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.DesiredReplicas), "desiredReplicas")...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.CurrentReplicas), validation.NewFieldPath("status", "currentReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.DesiredReplicas), validation.NewFieldPath("status", "desiredReplicasa"))...) return allErrs } func ValidateThirdPartyResourceUpdate(update, old *extensions.ThirdPartyResource) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, validation.NewFieldPath("metadata"))...) allErrs = append(allErrs, ValidateThirdPartyResource(update)...) return allErrs } @@ -120,16 +119,16 @@ func ValidateThirdPartyResourceName(name string, prefix bool) (bool, string) { func ValidateThirdPartyResource(obj *extensions.ThirdPartyResource) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateThirdPartyResourceName).Prefix("metadata")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateThirdPartyResourceName, validation.NewFieldPath("metadata"))...) versions := sets.String{} for ix := range obj.Versions { version := &obj.Versions[ix] if len(version.Name) == 0 { - allErrs = append(allErrs, validation.NewInvalidError("name", version, "name can not be empty")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("versions").Index(ix).Child("name"), version, "can not be empty")) } if versions.Has(version.Name) { - allErrs = append(allErrs, validation.NewDuplicateError("version", version)) + allErrs = append(allErrs, validation.NewDuplicateError(validation.NewFieldPath("versions").Index(ix).Child("name"), version)) } versions.Insert(version.Name) } @@ -139,39 +138,39 @@ func ValidateThirdPartyResource(obj *extensions.ThirdPartyResource) validation.E // ValidateDaemonSet tests if required fields in the DaemonSet are set. func ValidateDaemonSet(controller *extensions.DaemonSet) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&controller.ObjectMeta, true, ValidateDaemonSetName).Prefix("metadata")...) - allErrs = append(allErrs, ValidateDaemonSetSpec(&controller.Spec).Prefix("spec")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&controller.ObjectMeta, true, ValidateDaemonSetName, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateDaemonSetSpec(&controller.Spec, validation.NewFieldPath("spec"))...) return allErrs } // ValidateDaemonSetUpdate tests if required fields in the DaemonSet are set. func ValidateDaemonSetUpdate(controller, oldController *extensions.DaemonSet) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidateDaemonSetSpec(&controller.Spec).Prefix("spec")...) - allErrs = append(allErrs, ValidateDaemonSetTemplateUpdate(controller.Spec.Template, oldController.Spec.Template).Prefix("spec.template")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateDaemonSetSpec(&controller.Spec, validation.NewFieldPath("spec"))...) + allErrs = append(allErrs, ValidateDaemonSetTemplateUpdate(controller.Spec.Template, oldController.Spec.Template, validation.NewFieldPath("spec", "template"))...) return allErrs } // validateDaemonSetStatus validates a DaemonSetStatus -func validateDaemonSetStatus(status *extensions.DaemonSetStatus) validation.ErrorList { +func validateDaemonSetStatus(status *extensions.DaemonSetStatus, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.CurrentNumberScheduled), "currentNumberScheduled")...) - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.NumberMisscheduled), "numberMisscheduled")...) - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.DesiredNumberScheduled), "desiredNumberScheduled")...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.CurrentNumberScheduled), fldPath.Child("currentNumberScheduled"))...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.NumberMisscheduled), fldPath.Child("numberMisscheduled"))...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.DesiredNumberScheduled), fldPath.Child("desiredNumberScheduled"))...) return allErrs } // ValidateDaemonSetStatus validates tests if required fields in the DaemonSet Status section func ValidateDaemonSetStatusUpdate(controller, oldController *extensions.DaemonSet) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, validateDaemonSetStatus(&controller.Status)...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, validateDaemonSetStatus(&controller.Status, validation.NewFieldPath("status"))...) return allErrs } // ValidateDaemonSetTemplateUpdate tests that certain fields in the daemon set's pod template are not updated. -func ValidateDaemonSetTemplateUpdate(podTemplate, oldPodTemplate *api.PodTemplateSpec) validation.ErrorList { +func ValidateDaemonSetTemplateUpdate(podTemplate, oldPodTemplate *api.PodTemplateSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} podSpec := podTemplate.Spec // podTemplate.Spec is not a pointer, so we can modify NodeSelector and NodeName directly. @@ -180,33 +179,33 @@ func ValidateDaemonSetTemplateUpdate(podTemplate, oldPodTemplate *api.PodTemplat // In particular, we do not allow updates to container images at this point. if !api.Semantic.DeepEqual(oldPodTemplate.Spec, podSpec) { // TODO: Pinpoint the specific field that causes the invalid error after we have strategic merge diff - allErrs = append(allErrs, validation.NewInvalidError("spec", "content of spec is not printed out, please refer to the \"details\"", "may not update fields other than spec.nodeSelector")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("spec"), "content of spec is not printed out, please refer to the \"details\"", "may not update fields other than spec.nodeSelector")) } return allErrs } // ValidateDaemonSetSpec tests if required fields in the DaemonSetSpec are set. -func ValidateDaemonSetSpec(spec *extensions.DaemonSetSpec) validation.ErrorList { +func ValidateDaemonSetSpec(spec *extensions.DaemonSetSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidatePodSelector(spec.Selector)...) + allErrs = append(allErrs, ValidatePodSelector(spec.Selector, fldPath.Child("selector"))...) if spec.Template == nil { - allErrs = append(allErrs, validation.NewRequiredError("template")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("template"))) return allErrs } selector, err := extensions.PodSelectorAsSelector(spec.Selector) if err == nil && !selector.Matches(labels.Set(spec.Template.Labels)) { - allErrs = append(allErrs, validation.NewInvalidError("template.metadata.labels", spec.Template.Labels, "selector does not match template")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("template", "metadata", "labels"), spec.Template.Labels, "selector does not match template")) } - allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(spec.Template).Prefix("template")...) + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(spec.Template, fldPath.Child("template"))...) // Daemons typically run on more than one node, so mark Read-Write persistent disks as invalid. - allErrs = append(allErrs, apivalidation.ValidateReadOnlyPersistentDisks(spec.Template.Spec.Volumes).Prefix("template.spec.volumes")...) + allErrs = append(allErrs, apivalidation.ValidateReadOnlyPersistentDisks(spec.Template.Spec.Volumes, fldPath.Child("template", "spec", "volumes"))...) // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). if spec.Template.Spec.RestartPolicy != api.RestartPolicyAlways { - allErrs = append(allErrs, validation.NewNotSupportedError("template.spec.restartPolicy", spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) + allErrs = append(allErrs, validation.NewNotSupportedError(fldPath.Child("template", "spec", "restartPolicy"), spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) } return allErrs @@ -224,14 +223,14 @@ func ValidateDeploymentName(name string, prefix bool) (bool, string) { return apivalidation.NameIsDNSSubdomain(name, prefix) } -func ValidatePositiveIntOrPercent(intOrPercent intstr.IntOrString, fieldName string) validation.ErrorList { +func ValidatePositiveIntOrPercent(intOrPercent intstr.IntOrString, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if intOrPercent.Type == intstr.String { if !validation.IsValidPercent(intOrPercent.StrVal) { - allErrs = append(allErrs, validation.NewInvalidError(fieldName, intOrPercent, "value should be int(5) or percentage(5%)")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, intOrPercent, "value should be int(5) or percentage(5%)")) } } else if intOrPercent.Type == intstr.Int { - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(intOrPercent.IntVal), fieldName)...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(intOrPercent.IntValue()), fldPath)...) } return allErrs } @@ -252,69 +251,69 @@ func getIntOrPercentValue(intOrStringValue intstr.IntOrString) int { return intOrStringValue.IntValue() } -func IsNotMoreThan100Percent(intOrStringValue intstr.IntOrString, fieldName string) validation.ErrorList { +func IsNotMoreThan100Percent(intOrStringValue intstr.IntOrString, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} value, isPercent := getPercentValue(intOrStringValue) if !isPercent || value <= 100 { return nil } - allErrs = append(allErrs, validation.NewInvalidError(fieldName, intOrStringValue, "should not be more than 100%")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath, intOrStringValue, "should not be more than 100%")) return allErrs } -func ValidateRollingUpdateDeployment(rollingUpdate *extensions.RollingUpdateDeployment, fieldName string) validation.ErrorList { +func ValidateRollingUpdateDeployment(rollingUpdate *extensions.RollingUpdateDeployment, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxUnavailable, fieldName+"maxUnavailable")...) - allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxSurge, fieldName+".maxSurge")...) + allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) + allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxSurge, fldPath.Child("maxSurge"))...) if getIntOrPercentValue(rollingUpdate.MaxUnavailable) == 0 && getIntOrPercentValue(rollingUpdate.MaxSurge) == 0 { // Both MaxSurge and MaxUnavailable cannot be zero. - allErrs = append(allErrs, validation.NewInvalidError(fieldName+".maxUnavailable", rollingUpdate.MaxUnavailable, "cannot be 0 when maxSurge is 0 as well")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("maxUnavailable"), rollingUpdate.MaxUnavailable, "cannot be 0 when maxSurge is 0 as well")) } // Validate that MaxUnavailable is not more than 100%. - allErrs = append(allErrs, IsNotMoreThan100Percent(rollingUpdate.MaxUnavailable, fieldName+".maxUnavailable")...) - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(rollingUpdate.MinReadySeconds), fieldName+".minReadySeconds")...) + allErrs = append(allErrs, IsNotMoreThan100Percent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(rollingUpdate.MinReadySeconds), fldPath.Child("minReadySeconds"))...) return allErrs } -func ValidateDeploymentStrategy(strategy *extensions.DeploymentStrategy, fieldName string) validation.ErrorList { +func ValidateDeploymentStrategy(strategy *extensions.DeploymentStrategy, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if strategy.RollingUpdate == nil { return allErrs } switch strategy.Type { case extensions.RecreateDeploymentStrategyType: - allErrs = append(allErrs, validation.NewForbiddenError("rollingUpdate", "rollingUpdate should be nil when strategy type is "+extensions.RecreateDeploymentStrategyType)) + allErrs = append(allErrs, validation.NewForbiddenError(fldPath.Child("rollingUpdate"), "should be nil when strategy type is "+extensions.RecreateDeploymentStrategyType)) case extensions.RollingUpdateDeploymentStrategyType: - allErrs = append(allErrs, ValidateRollingUpdateDeployment(strategy.RollingUpdate, "rollingUpdate")...) + allErrs = append(allErrs, ValidateRollingUpdateDeployment(strategy.RollingUpdate, fldPath.Child("rollingUpdate"))...) } return allErrs } // Validates given deployment spec. -func ValidateDeploymentSpec(spec *extensions.DeploymentSpec) validation.ErrorList { +func ValidateDeploymentSpec(spec *extensions.DeploymentSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateNonEmptySelector(spec.Selector, "selector")...) - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(spec.Replicas), "replicas")...) - allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpecForRC(&spec.Template, spec.Selector, spec.Replicas, "template")...) - allErrs = append(allErrs, ValidateDeploymentStrategy(&spec.Strategy, "strategy")...) + allErrs = append(allErrs, apivalidation.ValidateNonEmptySelector(spec.Selector, fldPath.Child("selector"))...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(spec.Replicas), fldPath.Child("replicas"))...) + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpecForRC(&spec.Template, spec.Selector, spec.Replicas, fldPath.Child("template"))...) + allErrs = append(allErrs, ValidateDeploymentStrategy(&spec.Strategy, fldPath.Child("strategy"))...) // empty string is a valid UniqueLabelKey if len(spec.UniqueLabelKey) > 0 { - allErrs = append(allErrs, apivalidation.ValidateLabelName(spec.UniqueLabelKey, "uniqueLabel")...) + allErrs = append(allErrs, apivalidation.ValidateLabelName(spec.UniqueLabelKey, fldPath.Child("uniqueLabel"))...) } return allErrs } func ValidateDeploymentUpdate(update, old *extensions.Deployment) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidateDeploymentSpec(&update.Spec).Prefix("spec")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateDeploymentSpec(&update.Spec, validation.NewFieldPath("spec"))...) return allErrs } func ValidateDeployment(obj *extensions.Deployment) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateDeploymentName).Prefix("metadata")...) - allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec).Prefix("spec")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateDeploymentName, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec, validation.NewFieldPath("spec"))...) return allErrs } @@ -325,7 +324,7 @@ func ValidateThirdPartyResourceDataUpdate(update, old *extensions.ThirdPartyReso func ValidateThirdPartyResourceData(obj *extensions.ThirdPartyResourceData) validation.ErrorList { allErrs := validation.ErrorList{} if len(obj.Name) == 0 { - allErrs = append(allErrs, validation.NewInvalidError("name", obj.Name, "name must be non-empty")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("name"), obj.Name, "must be non-empty")) } return allErrs } @@ -333,84 +332,84 @@ func ValidateThirdPartyResourceData(obj *extensions.ThirdPartyResourceData) vali func ValidateJob(job *extensions.Job) validation.ErrorList { allErrs := validation.ErrorList{} // Jobs and rcs have the same name validation - allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName).Prefix("metadata")...) - allErrs = append(allErrs, ValidateJobSpec(&job.Spec).Prefix("spec")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateJobSpec(&job.Spec, validation.NewFieldPath("spec"))...) return allErrs } -func ValidateJobSpec(spec *extensions.JobSpec) validation.ErrorList { +func ValidateJobSpec(spec *extensions.JobSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if spec.Parallelism != nil { - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(*spec.Parallelism), "parallelism")...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(*spec.Parallelism), fldPath.Child("parallelism"))...) } if spec.Completions != nil { - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(*spec.Completions), "completions")...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(*spec.Completions), fldPath.Child("completions"))...) } if spec.Selector == nil { - allErrs = append(allErrs, validation.NewRequiredError("selector")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("selector"))) } else { - allErrs = append(allErrs, ValidatePodSelector(spec.Selector).Prefix("selector")...) + allErrs = append(allErrs, ValidatePodSelector(spec.Selector, fldPath.Child("selector"))...) } if selector, err := extensions.PodSelectorAsSelector(spec.Selector); err == nil { labels := labels.Set(spec.Template.Labels) if !selector.Matches(labels) { - allErrs = append(allErrs, validation.NewInvalidError("template.metadata.labels", spec.Template.Labels, "selector does not match template")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("template", "metadata", "labels"), spec.Template.Labels, "selector does not match template")) } } - allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template).Prefix("template")...) + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"))...) if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && spec.Template.Spec.RestartPolicy != api.RestartPolicyNever { - allErrs = append(allErrs, validation.NewNotSupportedError("template.spec.restartPolicy", + allErrs = append(allErrs, validation.NewNotSupportedError(fldPath.Child("template", "spec", "restartPolicy"), spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyOnFailure), string(api.RestartPolicyNever)})) } return allErrs } -func ValidateJobStatus(status *extensions.JobStatus) validation.ErrorList { +func ValidateJobStatus(status *extensions.JobStatus, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.Active), "active")...) - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.Succeeded), "succeeded")...) - allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.Failed), "failed")...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.Active), fldPath.Child("active"))...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.Succeeded), fldPath.Child("succeeded"))...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.Failed), fldPath.Child("failed"))...) return allErrs } func ValidateJobUpdate(job, oldJob *extensions.Job) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&oldJob.ObjectMeta, &job.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidateJobSpecUpdate(job.Spec, oldJob.Spec).Prefix("spec")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&oldJob.ObjectMeta, &job.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateJobSpecUpdate(job.Spec, oldJob.Spec, validation.NewFieldPath("spec"))...) return allErrs } func ValidateJobUpdateStatus(job, oldJob *extensions.Job) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&oldJob.ObjectMeta, &job.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidateJobStatusUpdate(job.Status, oldJob.Status).Prefix("status")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&oldJob.ObjectMeta, &job.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateJobStatusUpdate(job.Status, oldJob.Status)...) return allErrs } -func ValidateJobSpecUpdate(spec, oldSpec extensions.JobSpec) validation.ErrorList { +func ValidateJobSpecUpdate(spec, oldSpec extensions.JobSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateJobSpec(&spec)...) - allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, "completions")...) - allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Selector, oldSpec.Selector, "selector")...) - allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Template, oldSpec.Template, "template")...) + allErrs = append(allErrs, ValidateJobSpec(&spec, fldPath)...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath.Child("completions"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Selector, oldSpec.Selector, fldPath.Child("selector"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Template, oldSpec.Template, fldPath.Child("template"))...) return allErrs } func ValidateJobStatusUpdate(status, oldStatus extensions.JobStatus) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, ValidateJobStatus(&status)...) + allErrs = append(allErrs, ValidateJobStatus(&status, validation.NewFieldPath("status"))...) return allErrs } // ValidateIngress tests if required fields in the Ingress are set. func ValidateIngress(ingress *extensions.Ingress) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&ingress.ObjectMeta, true, ValidateIngressName).Prefix("metadata")...) - allErrs = append(allErrs, ValidateIngressSpec(&ingress.Spec).Prefix("spec")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&ingress.ObjectMeta, true, ValidateIngressName, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateIngressSpec(&ingress.Spec, validation.NewFieldPath("spec"))...) return allErrs } @@ -420,16 +419,16 @@ func ValidateIngressName(name string, prefix bool) (bool, string) { } // ValidateIngressSpec tests if required fields in the IngressSpec are set. -func ValidateIngressSpec(spec *extensions.IngressSpec) validation.ErrorList { +func ValidateIngressSpec(spec *extensions.IngressSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} // TODO: Is a default backend mandatory? if spec.Backend != nil { - allErrs = append(allErrs, validateIngressBackend(spec.Backend).Prefix("backend")...) + allErrs = append(allErrs, validateIngressBackend(spec.Backend, fldPath.Child("backend"))...) } else if len(spec.Rules) == 0 { - allErrs = append(allErrs, validation.NewInvalidError("rules", spec.Rules, "Either a default backend or a set of host rules are required for ingress.")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("rules"), spec.Rules, "Either a default backend or a set of host rules are required for ingress.")) } if len(spec.Rules) > 0 { - allErrs = append(allErrs, validateIngressRules(spec.Rules).Prefix("rules")...) + allErrs = append(allErrs, validateIngressRules(spec.Rules, fldPath.Child("rules"))...) } return allErrs } @@ -437,57 +436,57 @@ func ValidateIngressSpec(spec *extensions.IngressSpec) validation.ErrorList { // ValidateIngressUpdate tests if required fields in the Ingress are set. func ValidateIngressUpdate(ingress, oldIngress *extensions.Ingress) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidateIngressSpec(&ingress.Spec).Prefix("spec")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, ValidateIngressSpec(&ingress.Spec, validation.NewFieldPath("spec"))...) return allErrs } // ValidateIngressStatusUpdate tests if required fields in the Ingress are set when updating status. func ValidateIngressStatusUpdate(ingress, oldIngress *extensions.Ingress) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, apivalidation.ValidateLoadBalancerStatus(&ingress.Status.LoadBalancer).Prefix("status.loadBalancer")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta, validation.NewFieldPath("metadata"))...) + allErrs = append(allErrs, apivalidation.ValidateLoadBalancerStatus(&ingress.Status.LoadBalancer, validation.NewFieldPath("status", "loadBalancer"))...) return allErrs } -func validateIngressRules(IngressRules []extensions.IngressRule) validation.ErrorList { +func validateIngressRules(IngressRules []extensions.IngressRule, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(IngressRules) == 0 { - return append(allErrs, validation.NewRequiredError("IngressRules")) + return append(allErrs, validation.NewRequiredError(fldPath)) } - for _, ih := range IngressRules { + for i, ih := range IngressRules { if len(ih.Host) > 0 { // TODO: Ports and ips are allowed in the host part of a url // according to RFC 3986, consider allowing them. if valid, errMsg := apivalidation.NameIsDNSSubdomain(ih.Host, false); !valid { - allErrs = append(allErrs, validation.NewInvalidError("host", ih.Host, errMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Index(i).Child("host"), ih.Host, errMsg)) } if isIP := (net.ParseIP(ih.Host) != nil); isIP { - allErrs = append(allErrs, validation.NewInvalidError("host", ih.Host, "Host must be a DNS name, not ip address")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Index(i).Child("host"), ih.Host, "Host must be a DNS name, not ip address")) } } - allErrs = append(allErrs, validateIngressRuleValue(&ih.IngressRuleValue).Prefix("ingressRule")...) + allErrs = append(allErrs, validateIngressRuleValue(&ih.IngressRuleValue, fldPath.Index(0))...) } return allErrs } -func validateIngressRuleValue(ingressRule *extensions.IngressRuleValue) validation.ErrorList { +func validateIngressRuleValue(ingressRule *extensions.IngressRuleValue, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if ingressRule.HTTP != nil { - allErrs = append(allErrs, validateHTTPIngressRuleValue(ingressRule.HTTP).Prefix("http")...) + allErrs = append(allErrs, validateHTTPIngressRuleValue(ingressRule.HTTP, fldPath.Child("http"))...) } return allErrs } -func validateHTTPIngressRuleValue(httpIngressRuleValue *extensions.HTTPIngressRuleValue) validation.ErrorList { +func validateHTTPIngressRuleValue(httpIngressRuleValue *extensions.HTTPIngressRuleValue, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if len(httpIngressRuleValue.Paths) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("paths")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("paths"))) } - for _, rule := range httpIngressRuleValue.Paths { + for i, rule := range httpIngressRuleValue.Paths { if len(rule.Path) > 0 { if !strings.HasPrefix(rule.Path, "/") { - allErrs = append(allErrs, validation.NewInvalidError("path", rule.Path, "path must begin with /")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("paths").Index(i).Child("path"), rule.Path, "must begin with /")) } // TODO: More draconian path regex validation. // Path must be a valid regex. This is the basic requirement. @@ -500,57 +499,57 @@ func validateHTTPIngressRuleValue(httpIngressRuleValue *extensions.HTTPIngressRu // the user is confusing url regexes with path regexes. _, err := regexp.CompilePOSIX(rule.Path) if err != nil { - allErrs = append(allErrs, validation.NewInvalidError("path", rule.Path, "httpIngressRuleValue.path must be a valid regex.")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("paths").Index(i).Child("path"), rule.Path, "must be a valid regex.")) } } - allErrs = append(allErrs, validateIngressBackend(&rule.Backend).Prefix("backend")...) + allErrs = append(allErrs, validateIngressBackend(&rule.Backend, fldPath.Child("backend"))...) } return allErrs } // validateIngressBackend tests if a given backend is valid. -func validateIngressBackend(backend *extensions.IngressBackend) validation.ErrorList { +func validateIngressBackend(backend *extensions.IngressBackend, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} // All backends must reference a single local service by name, and a single service port by name or number. if len(backend.ServiceName) == 0 { - return append(allErrs, validation.NewRequiredError("serviceName")) + return append(allErrs, validation.NewRequiredError(fldPath.Child("serviceName"))) } else if ok, errMsg := apivalidation.ValidateServiceName(backend.ServiceName, false); !ok { - allErrs = append(allErrs, validation.NewInvalidError("serviceName", backend.ServiceName, errMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("serviceName"), backend.ServiceName, errMsg)) } if backend.ServicePort.Type == intstr.String { if !validation.IsDNS1123Label(backend.ServicePort.StrVal) { - allErrs = append(allErrs, validation.NewInvalidError("servicePort", backend.ServicePort.StrVal, apivalidation.DNS1123LabelErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("servicePort"), backend.ServicePort.StrVal, apivalidation.DNS1123LabelErrorMsg)) } if !validation.IsValidPortName(backend.ServicePort.StrVal) { - allErrs = append(allErrs, validation.NewInvalidError("servicePort", backend.ServicePort.StrVal, apivalidation.PortNameErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("servicePort"), backend.ServicePort.StrVal, apivalidation.PortNameErrorMsg)) } } else if !validation.IsValidPortNum(backend.ServicePort.IntValue()) { - allErrs = append(allErrs, validation.NewInvalidError("servicePort", backend.ServicePort, apivalidation.PortRangeErrorMsg)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("servicePort"), backend.ServicePort, apivalidation.PortRangeErrorMsg)) } return allErrs } -func validateClusterAutoscalerSpec(spec extensions.ClusterAutoscalerSpec) validation.ErrorList { +func validateClusterAutoscalerSpec(spec extensions.ClusterAutoscalerSpec, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if spec.MinNodes < 0 { - allErrs = append(allErrs, validation.NewInvalidError("minNodes", spec.MinNodes, `must be non-negative`)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("minNodes"), spec.MinNodes, `must be non-negative`)) } if spec.MaxNodes < spec.MinNodes { - allErrs = append(allErrs, validation.NewInvalidError("maxNodes", spec.MaxNodes, `must be bigger or equal to minNodes`)) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("maxNodes"), spec.MaxNodes, `must be greater than or equal to minNodes`)) } if len(spec.TargetUtilization) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("targetUtilization")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("targetUtilization"))) } for _, target := range spec.TargetUtilization { if len(target.Resource) == 0 { - allErrs = append(allErrs, validation.NewRequiredError("targetUtilization.resource")) + allErrs = append(allErrs, validation.NewRequiredError(fldPath.Child("targetUtilization", "resource"))) } if target.Value <= 0 { - allErrs = append(allErrs, validation.NewInvalidError("targetUtilization.value", target.Value, "must be greater than 0")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("targetUtilization", "value"), target.Value, "must be greater than 0")) } if target.Value > 1 { - allErrs = append(allErrs, validation.NewInvalidError("targetUtilization.value", target.Value, "must be less or equal 1")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("targetUtilization", "value"), target.Value, "must be less or equal 1")) } } return allErrs @@ -559,51 +558,51 @@ func validateClusterAutoscalerSpec(spec extensions.ClusterAutoscalerSpec) valida func ValidateClusterAutoscaler(autoscaler *extensions.ClusterAutoscaler) validation.ErrorList { allErrs := validation.ErrorList{} if autoscaler.Name != "ClusterAutoscaler" { - allErrs = append(allErrs, validation.NewInvalidError("name", autoscaler.Name, `name must be ClusterAutoscaler`)) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("metadata", "name"), autoscaler.Name, `name must be ClusterAutoscaler`)) } if autoscaler.Namespace != api.NamespaceDefault { - allErrs = append(allErrs, validation.NewInvalidError("namespace", autoscaler.Namespace, `namespace must be default`)) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("metadata", "namespace"), autoscaler.Namespace, `namespace must be default`)) } - allErrs = append(allErrs, validateClusterAutoscalerSpec(autoscaler.Spec)...) + allErrs = append(allErrs, validateClusterAutoscalerSpec(autoscaler.Spec, validation.NewFieldPath("spec"))...) return allErrs } -func ValidatePodSelector(ps *extensions.PodSelector) validation.ErrorList { +func ValidatePodSelector(ps *extensions.PodSelector, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} if ps == nil { return allErrs } - allErrs = append(allErrs, apivalidation.ValidateLabels(ps.MatchLabels, "matchLabels")...) + allErrs = append(allErrs, apivalidation.ValidateLabels(ps.MatchLabels, fldPath.Child("matchLabels"))...) for i, expr := range ps.MatchExpressions { - allErrs = append(allErrs, ValidatePodSelectorRequirement(expr).Prefix(fmt.Sprintf("matchExpressions.[%v]", i))...) + allErrs = append(allErrs, ValidatePodSelectorRequirement(expr, fldPath.Child("matchExpressions").Index(i))...) } return allErrs } -func ValidatePodSelectorRequirement(sr extensions.PodSelectorRequirement) validation.ErrorList { +func ValidatePodSelectorRequirement(sr extensions.PodSelectorRequirement, fldPath *validation.FieldPath) validation.ErrorList { allErrs := validation.ErrorList{} switch sr.Operator { case extensions.PodSelectorOpIn, extensions.PodSelectorOpNotIn: if len(sr.Values) == 0 { - allErrs = append(allErrs, validation.NewInvalidError("values", sr.Values, "must be non-empty when operator is In or NotIn")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("values"), sr.Values, "must be non-empty when operator is In or NotIn")) } case extensions.PodSelectorOpExists, extensions.PodSelectorOpDoesNotExist: if len(sr.Values) > 0 { - allErrs = append(allErrs, validation.NewInvalidError("values", sr.Values, "must be empty when operator is Exists or DoesNotExist")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("values"), sr.Values, "must be empty when operator is Exists or DoesNotExist")) } default: - allErrs = append(allErrs, validation.NewInvalidError("operator", sr.Operator, "not a valid pod selector operator")) + allErrs = append(allErrs, validation.NewInvalidError(fldPath.Child("operator"), sr.Operator, "not a valid pod selector operator")) } - allErrs = append(allErrs, apivalidation.ValidateLabelName(sr.Key, "key")...) + allErrs = append(allErrs, apivalidation.ValidateLabelName(sr.Key, fldPath.Child("key"))...) return allErrs } func ValidateScale(scale *extensions.Scale) validation.ErrorList { allErrs := validation.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&scale.ObjectMeta, true, apivalidation.NameIsDNSSubdomain).Prefix("metadata")...) + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&scale.ObjectMeta, true, apivalidation.NameIsDNSSubdomain, validation.NewFieldPath("metadata"))...) if scale.Spec.Replicas < 0 { - allErrs = append(allErrs, validation.NewInvalidError("spec.replicas", scale.Spec.Replicas, "must be non-negative")) + allErrs = append(allErrs, validation.NewInvalidError(validation.NewFieldPath("spec", "replicas"), scale.Spec.Replicas, "must be non-negative")) } return allErrs diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index 497e88e88fc..49b18940cee 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -168,7 +168,7 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { MaxReplicas: 5, }, }, - msg: "must be bigger or equal to 1", + msg: "must be greater than or equal to 1", }, { horizontalPodAutoscaler: extensions.HorizontalPodAutoscaler{ @@ -184,7 +184,7 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { MaxReplicas: 5, }, }, - msg: "must be bigger or equal to minReplicas", + msg: "must be greater than or equal to minReplicas", }, { horizontalPodAutoscaler: extensions.HorizontalPodAutoscaler{ @@ -201,16 +201,16 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { CPUUtilization: &extensions.CPUTargetUtilization{TargetPercentage: -70}, }, }, - msg: "must be bigger or equal to 1", + msg: "must be greater than or equal to 1", }, } for _, c := range errorCases { errs := ValidateHorizontalPodAutoscaler(&c.horizontalPodAutoscaler) if len(errs) == 0 { - t.Errorf("expected failure for %s", c.msg) + t.Errorf("expected failure for %q", c.msg) } else if !strings.Contains(errs[0].Error(), c.msg) { - t.Errorf("unexpected error: %v, expected: %s", errs[0], c.msg) + t.Errorf("unexpected error: %q, expected: %q", errs[0], c.msg) } } } @@ -764,7 +764,7 @@ func TestValidateDeployment(t *testing.T) { Type: extensions.RecreateDeploymentStrategyType, RollingUpdate: &extensions.RollingUpdateDeployment{}, } - errorCases["rollingUpdate should be nil when strategy type is Recreate"] = invalidRecreateDeployment + errorCases["should be nil when strategy type is Recreate"] = invalidRecreateDeployment // MaxSurge should be in the form of 20%. invalidMaxSurgeDeployment := validDeployment() @@ -994,20 +994,19 @@ func TestValidateIngress(t *testing.T) { Backend: defaultBackend, }, } - badPathErr := fmt.Sprintf("spec.rules.ingressRule.http.path: invalid value '%v'", - badPathExpr) + badPathErr := fmt.Sprintf("spec.rules[0].http.paths[0].path: invalid value '%v'", badPathExpr) hostIP := "127.0.0.1" badHostIP := newValid() badHostIP.Spec.Rules[0].Host = hostIP - badHostIPErr := fmt.Sprintf("spec.rules.host: invalid value '%v'", hostIP) + badHostIPErr := fmt.Sprintf("spec.rules[0].host: invalid value '%v'", hostIP) errorCases := map[string]extensions.Ingress{ - "spec.backend.serviceName: required value": servicelessBackend, - "spec.backend.serviceName: invalid value": invalidNameBackend, - "spec.backend.servicePort: invalid value": noPortBackend, - "spec.rules.host: invalid value": badHost, - "spec.rules.ingressRule.http.paths: required value": noPaths, - "spec.rules.ingressRule.http.path: invalid value": noForwardSlashPath, + "spec.backend.serviceName: required value": servicelessBackend, + "spec.backend.serviceName: invalid value": invalidNameBackend, + "spec.backend.servicePort: invalid value": noPortBackend, + "spec.rules[0].host: invalid value": badHost, + "spec.rules[0].http.paths: required value": noPaths, + "spec.rules[0].http.paths[0].path: invalid value": noForwardSlashPath, } errorCases[badPathErr] = badRegexPath errorCases[badHostIPErr] = badHostIP @@ -1015,12 +1014,12 @@ func TestValidateIngress(t *testing.T) { for k, v := range errorCases { errs := ValidateIngress(&v) if len(errs) == 0 { - t.Errorf("expected failure for %s", k) + t.Errorf("expected failure for %q", k) } else { s := strings.Split(k, ":") err := errs[0] if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { - t.Errorf("unexpected error: %v, expected: %s", err, k) + t.Errorf("unexpected error: %q, expected: %q", err, k) } } } @@ -1101,8 +1100,8 @@ func TestValidateIngressStatusUpdate(t *testing.T) { } errorCases := map[string]extensions.Ingress{ - "status.loadBalancer.ingress.ip: invalid value": invalidIP, - "status.loadBalancer.ingress.hostname: invalid value": invalidHostname, + "status.loadBalancer.ingress[0].ip: invalid value": invalidIP, + "status.loadBalancer.ingress[0].hostname: invalid value": invalidHostname, } for k, v := range errorCases { errs := ValidateIngressStatusUpdate(&v, &oldValue) @@ -1112,7 +1111,7 @@ func TestValidateIngressStatusUpdate(t *testing.T) { s := strings.Split(k, ":") err := errs[0] if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { - t.Errorf("unexpected error: %v, expected: %s", err, k) + t.Errorf("unexpected error: %q, expected: %q", err, k) } } } @@ -1193,7 +1192,7 @@ func TestValidateClusterAutoscaler(t *testing.T) { }, }, }, - `must be bigger or equal to minNodes`: { + `must be greater than or equal to minNodes`: { ObjectMeta: api.ObjectMeta{ Name: "ClusterAutoscaler", Namespace: api.NamespaceDefault, diff --git a/pkg/kubectl/cmd/util/helpers_test.go b/pkg/kubectl/cmd/util/helpers_test.go index 72db1ff9225..d069a92a808 100644 --- a/pkg/kubectl/cmd/util/helpers_test.go +++ b/pkg/kubectl/cmd/util/helpers_test.go @@ -274,12 +274,12 @@ func TestCheckInvalidErr(t *testing.T) { expected string }{ { - errors.NewInvalid("Invalid1", "invalidation", validation.ErrorList{validation.NewInvalidError("Cause", "single", "details")}), - `Error from server: Invalid1 "invalidation" is invalid: Cause: invalid value 'single', Details: details`, + errors.NewInvalid("Invalid1", "invalidation", validation.ErrorList{validation.NewInvalidError(validation.NewFieldPath("field"), "single", "details")}), + `Error from server: Invalid1 "invalidation" is invalid: field: invalid value 'single', Details: details`, }, { - errors.NewInvalid("Invalid2", "invalidation", validation.ErrorList{validation.NewInvalidError("Cause", "multi1", "details"), validation.NewInvalidError("Cause", "multi2", "details")}), - `Error from server: Invalid2 "invalidation" is invalid: [Cause: invalid value 'multi1', Details: details, Cause: invalid value 'multi2', Details: details]`, + errors.NewInvalid("Invalid2", "invalidation", validation.ErrorList{validation.NewInvalidError(validation.NewFieldPath("field1"), "multi1", "details"), validation.NewInvalidError(validation.NewFieldPath("field2"), "multi2", "details")}), + `Error from server: Invalid2 "invalidation" is invalid: [field1: invalid value 'multi1', Details: details, field2: invalid value 'multi2', Details: details]`, }, { errors.NewInvalid("Invalid3", "invalidation", validation.ErrorList{}), diff --git a/pkg/kubelet/config/config.go b/pkg/kubelet/config/config.go index c851adcaccf..7af4ddff06b 100644 --- a/pkg/kubelet/config/config.go +++ b/pkg/kubelet/config/config.go @@ -317,7 +317,8 @@ func filterInvalidPods(pods []*api.Pod, source string, recorder record.EventReco } else { name := kubecontainer.GetPodFullName(pod) if names.Has(name) { - errlist = append(errlist, utilvalidation.NewDuplicateError("name", pod.Name)) + //FIXME: this implies an API version + errlist = append(errlist, utilvalidation.NewDuplicateError(utilvalidation.NewFieldPath("metadata", "name"), pod.Name)) } else { names.Insert(name) } diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index c6fdb39e3e0..7ce87930206 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -77,6 +77,7 @@ import ( "k8s.io/kubernetes/pkg/util/procfs" "k8s.io/kubernetes/pkg/util/selinux" "k8s.io/kubernetes/pkg/util/sets" + utilvalidation "k8s.io/kubernetes/pkg/util/validation" "k8s.io/kubernetes/pkg/util/yaml" "k8s.io/kubernetes/pkg/version" "k8s.io/kubernetes/pkg/volume" @@ -2163,7 +2164,7 @@ func (s podsByCreationTime) Less(i, j int) bool { func hasHostPortConflicts(pods []*api.Pod) bool { ports := sets.String{} for _, pod := range pods { - if errs := validation.AccumulateUniqueHostPorts(pod.Spec.Containers, &ports); len(errs) > 0 { + if errs := validation.AccumulateUniqueHostPorts(pod.Spec.Containers, &ports, utilvalidation.NewFieldPath("spec", "containers")); len(errs) > 0 { glog.Errorf("Pod %q: HostPort is already allocated, ignoring: %v", kubecontainer.GetPodFullName(pod), errs) return true } diff --git a/pkg/labels/selector.go b/pkg/labels/selector.go index bc80dcb9cbe..301ff360132 100644 --- a/pkg/labels/selector.go +++ b/pkg/labels/selector.go @@ -701,14 +701,15 @@ const qualifiedNameErrorMsg string = "must match regex [" + validation.DNS1123Su func validateLabelKey(k string) error { if !validation.IsQualifiedName(k) { - return validation.NewInvalidError("label key", k, qualifiedNameErrorMsg) + return fmt.Errorf("invalid label key: %s", qualifiedNameErrorMsg) } return nil } func validateLabelValue(v string) error { if !validation.IsValidLabelValue(v) { - return validation.NewInvalidError("label value", v, qualifiedNameErrorMsg) + //FIXME: this is not the right regex! + return fmt.Errorf("invalid label value: %s", qualifiedNameErrorMsg) } return nil } diff --git a/pkg/registry/pod/etcd/etcd.go b/pkg/registry/pod/etcd/etcd.go index 0da73489f74..3584a5b866f 100644 --- a/pkg/registry/pod/etcd/etcd.go +++ b/pkg/registry/pod/etcd/etcd.go @@ -134,10 +134,10 @@ func (r *BindingREST) Create(ctx api.Context, obj runtime.Object) (out runtime.O binding := obj.(*api.Binding) // TODO: move me to a binding strategy if len(binding.Target.Kind) != 0 && binding.Target.Kind != "Node" { - return nil, errors.NewInvalid("binding", binding.Name, validation.ErrorList{validation.NewInvalidError("to.kind", binding.Target.Kind, "must be empty or 'Node'")}) + return nil, errors.NewInvalid("binding", binding.Name, validation.ErrorList{validation.NewInvalidError(validation.NewFieldPath("target", "kind"), binding.Target.Kind, "must be empty or 'Node'")}) } if len(binding.Target.Name) == 0 { - return nil, errors.NewInvalid("binding", binding.Name, validation.ErrorList{validation.NewRequiredError("to.name")}) + return nil, errors.NewInvalid("binding", binding.Name, validation.ErrorList{validation.NewRequiredError(validation.NewFieldPath("target", "name"))}) } err = r.assignPod(ctx, binding.Name, binding.Target.Name, binding.Annotations) out = &unversioned.Status{Status: unversioned.StatusSuccess} diff --git a/pkg/registry/service/rest.go b/pkg/registry/service/rest.go index 0d156ed92a5..079240d2531 100644 --- a/pkg/registry/service/rest.go +++ b/pkg/registry/service/rest.go @@ -84,7 +84,7 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err // Allocate next available. ip, err := rs.serviceIPs.AllocateNext() if err != nil { - el := utilvalidation.ErrorList{utilvalidation.NewInvalidError("spec.clusterIP", service.Spec.ClusterIP, err.Error())} + el := utilvalidation.ErrorList{utilvalidation.NewInvalidError(utilvalidation.NewFieldPath("spec", "clusterIP"), service.Spec.ClusterIP, err.Error())} return nil, errors.NewInvalid("Service", service.Name, el) } service.Spec.ClusterIP = ip.String() @@ -92,7 +92,7 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err } else if api.IsServiceIPSet(service) { // Try to respect the requested IP. if err := rs.serviceIPs.Allocate(net.ParseIP(service.Spec.ClusterIP)); err != nil { - el := utilvalidation.ErrorList{utilvalidation.NewInvalidError("spec.clusterIP", service.Spec.ClusterIP, err.Error())} + el := utilvalidation.ErrorList{utilvalidation.NewInvalidError(utilvalidation.NewFieldPath("spec", "clusterIP"), service.Spec.ClusterIP, err.Error())} return nil, errors.NewInvalid("Service", service.Name, el) } releaseServiceIP = true @@ -104,13 +104,13 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err if servicePort.NodePort != 0 { err := nodePortOp.Allocate(servicePort.NodePort) if err != nil { - el := utilvalidation.ErrorList{utilvalidation.NewInvalidError("nodePort", servicePort.NodePort, err.Error())}.PrefixIndex(i).Prefix("spec.ports") + el := utilvalidation.ErrorList{utilvalidation.NewInvalidError(utilvalidation.NewFieldPath("spec", "ports").Index(i).Child("nodePort"), servicePort.NodePort, err.Error())} return nil, errors.NewInvalid("Service", service.Name, el) } } else if assignNodePorts { nodePort, err := nodePortOp.AllocateNext() if err != nil { - el := utilvalidation.ErrorList{utilvalidation.NewInvalidError("nodePort", servicePort.NodePort, err.Error())}.PrefixIndex(i).Prefix("spec.ports") + el := utilvalidation.ErrorList{utilvalidation.NewInvalidError(utilvalidation.NewFieldPath("spec", "ports").Index(i).Child("nodePort"), servicePort.NodePort, err.Error())} return nil, errors.NewInvalid("Service", service.Name, el) } servicePort.NodePort = nodePort @@ -223,14 +223,14 @@ func (rs *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, boo if !contains(oldNodePorts, nodePort) { err := nodePortOp.Allocate(nodePort) if err != nil { - el := utilvalidation.ErrorList{utilvalidation.NewInvalidError("nodePort", nodePort, err.Error())}.PrefixIndex(i).Prefix("spec.ports") + el := utilvalidation.ErrorList{utilvalidation.NewInvalidError(utilvalidation.NewFieldPath("spec", "ports").Index(i).Child("nodePort"), nodePort, err.Error())} return nil, false, errors.NewInvalid("Service", service.Name, el) } } } else { nodePort, err = nodePortOp.AllocateNext() if err != nil { - el := utilvalidation.ErrorList{utilvalidation.NewInvalidError("nodePort", nodePort, err.Error())}.PrefixIndex(i).Prefix("spec.ports") + el := utilvalidation.ErrorList{utilvalidation.NewInvalidError(utilvalidation.NewFieldPath("spec", "ports").Index(i).Child("nodePort"), nodePort, err.Error())} return nil, false, errors.NewInvalid("Service", service.Name, el) } servicePort.NodePort = nodePort diff --git a/pkg/storage/util.go b/pkg/storage/util.go index 8f8aa5277df..0b8fe7a3b0e 100644 --- a/pkg/storage/util.go +++ b/pkg/storage/util.go @@ -47,8 +47,11 @@ func ParseWatchResourceVersion(resourceVersion, kind string) (uint64, error) { } version, err := strconv.ParseUint(resourceVersion, 10, 64) if err != nil { - // TODO: Does this need to be a ErrorList? I can't convince myself it does. - return 0, errors.NewInvalid(kind, "", utilvalidation.ErrorList{utilvalidation.NewInvalidError("resourceVersion", resourceVersion, err.Error())}) + return 0, errors.NewInvalid(kind, "", utilvalidation.ErrorList{ + // Validation errors are supposed to return version-specific field + // paths, but this is probably close enough. + utilvalidation.NewInvalidError(utilvalidation.NewFieldPath("resourceVersion"), resourceVersion, err.Error()), + }) } return version + 1, nil } diff --git a/pkg/util/validation/errors.go b/pkg/util/validation/errors.go index bb6bfd56164..381b9659074 100644 --- a/pkg/util/validation/errors.go +++ b/pkg/util/validation/errors.go @@ -121,61 +121,61 @@ func (t ErrorType) String() string { // NewNotFoundError returns a *Error indicating "value not found". This is // used to report failure to find a requested value (e.g. looking up an ID). -func NewNotFoundError(field string, value interface{}) *Error { - return &Error{ErrorTypeNotFound, field, value, ""} +func NewNotFoundError(field *FieldPath, value interface{}) *Error { + return &Error{ErrorTypeNotFound, field.String(), value, ""} } // NewRequiredError returns a *Error indicating "value required". This is used // to report required values that are not provided (e.g. empty strings, null // values, or empty arrays). -func NewRequiredError(field string) *Error { - return &Error{ErrorTypeRequired, field, "", ""} +func NewRequiredError(field *FieldPath) *Error { + return &Error{ErrorTypeRequired, field.String(), "", ""} } // NewDuplicateError returns a *Error indicating "duplicate value". This is // used to report collisions of values that must be unique (e.g. names or IDs). -func NewDuplicateError(field string, value interface{}) *Error { - return &Error{ErrorTypeDuplicate, field, value, ""} +func NewDuplicateError(field *FieldPath, value interface{}) *Error { + return &Error{ErrorTypeDuplicate, field.String(), value, ""} } // NewInvalidError returns a *Error indicating "invalid value". This is used // to report malformed values (e.g. failed regex match, too long, out of bounds). -func NewInvalidError(field string, value interface{}, detail string) *Error { - return &Error{ErrorTypeInvalid, field, value, detail} +func NewInvalidError(field *FieldPath, value interface{}, detail string) *Error { + return &Error{ErrorTypeInvalid, field.String(), value, detail} } // NewNotSupportedError returns a *Error indicating "unsupported value". // This is used to report unknown values for enumerated fields (e.g. a list of // valid values). -func NewNotSupportedError(field string, value interface{}, validValues []string) *Error { +func NewNotSupportedError(field *FieldPath, value interface{}, validValues []string) *Error { detail := "" if validValues != nil && len(validValues) > 0 { detail = "supported values: " + strings.Join(validValues, ", ") } - return &Error{ErrorTypeNotSupported, field, value, detail} + return &Error{ErrorTypeNotSupported, field.String(), value, detail} } // NewForbiddenError returns a *Error indicating "forbidden". This is used to // report valid (as per formatting rules) values which would be accepted under // some conditions, but which are not permitted by current conditions (e.g. // security policy). -func NewForbiddenError(field string, value interface{}) *Error { - return &Error{ErrorTypeForbidden, field, value, ""} +func NewForbiddenError(field *FieldPath, value interface{}) *Error { + return &Error{ErrorTypeForbidden, field.String(), value, ""} } // NewTooLongError returns a *Error indicating "too long". This is used to // report that the given value is too long. This is similar to // NewInvalidError, but the returned error will not include the too-long // value. -func NewTooLongError(field string, value interface{}, maxLength int) *Error { - return &Error{ErrorTypeTooLong, field, value, fmt.Sprintf("must have at most %d characters", maxLength)} +func NewTooLongError(field *FieldPath, value interface{}, maxLength int) *Error { + return &Error{ErrorTypeTooLong, field.String(), value, fmt.Sprintf("must have at most %d characters", maxLength)} } // NewInternalError returns a *Error indicating "internal error". This is used // to signal that an error was found that was not directly related to user // input. The err argument must be non-nil. -func NewInternalError(field string, err error) *Error { - return &Error{ErrorTypeInternal, field, nil, err.Error()} +func NewInternalError(field *FieldPath, err error) *Error { + return &Error{ErrorTypeInternal, field.String(), nil, err.Error()} } // ErrorList holds a set of errors. diff --git a/pkg/util/validation/errors_test.go b/pkg/util/validation/errors_test.go index 993e3a38d6b..ae115648b9f 100644 --- a/pkg/util/validation/errors_test.go +++ b/pkg/util/validation/errors_test.go @@ -28,27 +28,27 @@ func TestMakeFuncs(t *testing.T) { expected ErrorType }{ { - func() *Error { return NewInvalidError("f", "v", "d") }, + func() *Error { return NewInvalidError(NewFieldPath("f"), "v", "d") }, ErrorTypeInvalid, }, { - func() *Error { return NewNotSupportedError("f", "v", nil) }, + func() *Error { return NewNotSupportedError(NewFieldPath("f"), "v", nil) }, ErrorTypeNotSupported, }, { - func() *Error { return NewDuplicateError("f", "v") }, + func() *Error { return NewDuplicateError(NewFieldPath("f"), "v") }, ErrorTypeDuplicate, }, { - func() *Error { return NewNotFoundError("f", "v") }, + func() *Error { return NewNotFoundError(NewFieldPath("f"), "v") }, ErrorTypeNotFound, }, { - func() *Error { return NewRequiredError("f") }, + func() *Error { return NewRequiredError(NewFieldPath("f")) }, ErrorTypeRequired, }, { - func() *Error { return NewInternalError("f", fmt.Errorf("e")) }, + func() *Error { return NewInternalError(NewFieldPath("f"), fmt.Errorf("e")) }, ErrorTypeInternal, }, } @@ -62,7 +62,7 @@ func TestMakeFuncs(t *testing.T) { } func TestErrorUsefulMessage(t *testing.T) { - s := NewInvalidError("foo", "bar", "deet").Error() + s := NewInvalidError(NewFieldPath("foo"), "bar", "deet").Error() t.Logf("message: %v", s) for _, part := range []string{"foo", "bar", "deet", ErrorTypeInvalid.String()} { if !strings.Contains(s, part) { @@ -77,7 +77,7 @@ func TestErrorUsefulMessage(t *testing.T) { KV map[string]int } s = NewInvalidError( - "foo", + NewFieldPath("foo"), &complicated{ Baz: 1, Qux: "aoeu", @@ -102,8 +102,8 @@ func TestToAggregate(t *testing.T) { testCases := []ErrorList{ nil, {}, - {NewInvalidError("f", "v", "d")}, - {NewInvalidError("f", "v", "d"), NewInternalError("", fmt.Errorf("e"))}, + {NewInvalidError(NewFieldPath("f"), "v", "d")}, + {NewInvalidError(NewFieldPath("f"), "v", "d"), NewInternalError(NewFieldPath(""), fmt.Errorf("e"))}, } for i, tc := range testCases { agg := tc.ToAggregate() @@ -121,9 +121,9 @@ func TestToAggregate(t *testing.T) { func TestErrListFilter(t *testing.T) { list := ErrorList{ - NewInvalidError("test.field", "", ""), - NewInvalidError("field.test", "", ""), - NewDuplicateError("test", "value"), + NewInvalidError(NewFieldPath("test.field"), "", ""), + NewInvalidError(NewFieldPath("field.test"), "", ""), + NewDuplicateError(NewFieldPath("test"), "value"), } if len(list.Filter(NewErrorTypeMatcher(ErrorTypeDuplicate))) != 2 { t.Errorf("should not filter")