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")