mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-14 06:15:45 +00:00
Merge pull request #122550 from danwinship/tighten-ip-validation
Tighten IP/CIDR validation
This commit is contained in:
commit
2261137135
@ -3790,7 +3790,7 @@ func validatePodDNSConfig(dnsConfig *core.PodDNSConfig, dnsPolicy *core.DNSPolic
|
|||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("nameservers"), dnsConfig.Nameservers, fmt.Sprintf("must not have more than %v nameservers", MaxDNSNameservers)))
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("nameservers"), dnsConfig.Nameservers, fmt.Sprintf("must not have more than %v nameservers", MaxDNSNameservers)))
|
||||||
}
|
}
|
||||||
for i, ns := range dnsConfig.Nameservers {
|
for i, ns := range dnsConfig.Nameservers {
|
||||||
allErrs = append(allErrs, validation.IsValidIP(fldPath.Child("nameservers").Index(i), ns)...)
|
allErrs = append(allErrs, IsValidIPForLegacyField(fldPath.Child("nameservers").Index(i), ns, nil)...)
|
||||||
}
|
}
|
||||||
// Validate searches.
|
// Validate searches.
|
||||||
if len(dnsConfig.Searches) > MaxDNSSearchPaths {
|
if len(dnsConfig.Searches) > MaxDNSSearchPaths {
|
||||||
@ -3966,7 +3966,7 @@ func validateOnlyDeletedSchedulingGates(newGates, oldGates []core.PodSchedulingG
|
|||||||
func ValidateHostAliases(hostAliases []core.HostAlias, fldPath *field.Path) field.ErrorList {
|
func ValidateHostAliases(hostAliases []core.HostAlias, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
for i, hostAlias := range hostAliases {
|
for i, hostAlias := range hostAliases {
|
||||||
allErrs = append(allErrs, validation.IsValidIP(fldPath.Index(i).Child("ip"), hostAlias.IP)...)
|
allErrs = append(allErrs, IsValidIPForLegacyField(fldPath.Index(i).Child("ip"), hostAlias.IP, nil)...)
|
||||||
for j, hostname := range hostAlias.Hostnames {
|
for j, hostname := range hostAlias.Hostnames {
|
||||||
allErrs = append(allErrs, ValidateDNS1123Subdomain(hostname, fldPath.Index(i).Child("hostnames").Index(j))...)
|
allErrs = append(allErrs, ValidateDNS1123Subdomain(hostname, fldPath.Index(i).Child("hostnames").Index(j))...)
|
||||||
}
|
}
|
||||||
@ -4108,14 +4108,21 @@ func validatePodMetadataAndSpec(pod *core.Pod, opts PodValidationOptions) field.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validatePodIPs validates IPs in pod status
|
// validatePodIPs validates IPs in pod status
|
||||||
func validatePodIPs(pod *core.Pod) field.ErrorList {
|
func validatePodIPs(pod, oldPod *core.Pod) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
podIPsField := field.NewPath("status", "podIPs")
|
podIPsField := field.NewPath("status", "podIPs")
|
||||||
|
|
||||||
// all PodIPs must be valid IPs
|
// all new PodIPs must be valid IPs, but existing invalid ones can be kept.
|
||||||
|
var existingPodIPs []string
|
||||||
|
if oldPod != nil {
|
||||||
|
existingPodIPs = make([]string, len(oldPod.Status.PodIPs))
|
||||||
|
for i, podIP := range oldPod.Status.PodIPs {
|
||||||
|
existingPodIPs[i] = podIP.IP
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, podIP := range pod.Status.PodIPs {
|
for i, podIP := range pod.Status.PodIPs {
|
||||||
allErrs = append(allErrs, validation.IsValidIP(podIPsField.Index(i), podIP.IP)...)
|
allErrs = append(allErrs, IsValidIPForLegacyField(podIPsField.Index(i), podIP.IP, existingPodIPs)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we have more than one Pod.PodIP then we must have a dual-stack pair
|
// if we have more than one Pod.PodIP then we must have a dual-stack pair
|
||||||
@ -4140,7 +4147,7 @@ func validatePodIPs(pod *core.Pod) field.ErrorList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateHostIPs validates IPs in pod status
|
// validateHostIPs validates IPs in pod status
|
||||||
func validateHostIPs(pod *core.Pod) field.ErrorList {
|
func validateHostIPs(pod, oldPod *core.Pod) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
if len(pod.Status.HostIPs) == 0 {
|
if len(pod.Status.HostIPs) == 0 {
|
||||||
@ -4154,9 +4161,16 @@ func validateHostIPs(pod *core.Pod) field.ErrorList {
|
|||||||
allErrs = append(allErrs, field.Invalid(hostIPsField.Index(0).Child("ip"), pod.Status.HostIPs[0].IP, "must be equal to `hostIP`"))
|
allErrs = append(allErrs, field.Invalid(hostIPsField.Index(0).Child("ip"), pod.Status.HostIPs[0].IP, "must be equal to `hostIP`"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// all HostIPs must be valid IPs
|
// all new HostIPs must be valid IPs, but existing invalid ones can be kept.
|
||||||
|
var existingHostIPs []string
|
||||||
|
if oldPod != nil {
|
||||||
|
existingHostIPs = make([]string, len(oldPod.Status.HostIPs))
|
||||||
|
for i, hostIP := range oldPod.Status.HostIPs {
|
||||||
|
existingHostIPs[i] = hostIP.IP
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, hostIP := range pod.Status.HostIPs {
|
for i, hostIP := range pod.Status.HostIPs {
|
||||||
allErrs = append(allErrs, validation.IsValidIP(hostIPsField.Index(i), hostIP.IP)...)
|
allErrs = append(allErrs, IsValidIPForLegacyField(hostIPsField.Index(i), hostIP.IP, existingHostIPs)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we have more than one Pod.HostIP then we must have a dual-stack pair
|
// if we have more than one Pod.HostIP then we must have a dual-stack pair
|
||||||
@ -5462,11 +5476,11 @@ func ValidatePodStatusUpdate(newPod, oldPod *core.Pod, opts PodValidationOptions
|
|||||||
allErrs = append(allErrs, ValidateContainerStateTransition(newPod.Status.EphemeralContainerStatuses, oldPod.Status.EphemeralContainerStatuses, fldPath.Child("ephemeralContainerStatuses"), core.RestartPolicyNever)...)
|
allErrs = append(allErrs, ValidateContainerStateTransition(newPod.Status.EphemeralContainerStatuses, oldPod.Status.EphemeralContainerStatuses, fldPath.Child("ephemeralContainerStatuses"), core.RestartPolicyNever)...)
|
||||||
allErrs = append(allErrs, validatePodResourceClaimStatuses(newPod.Status.ResourceClaimStatuses, newPod.Spec.ResourceClaims, fldPath.Child("resourceClaimStatuses"))...)
|
allErrs = append(allErrs, validatePodResourceClaimStatuses(newPod.Status.ResourceClaimStatuses, newPod.Spec.ResourceClaims, fldPath.Child("resourceClaimStatuses"))...)
|
||||||
|
|
||||||
if newIPErrs := validatePodIPs(newPod); len(newIPErrs) > 0 {
|
if newIPErrs := validatePodIPs(newPod, oldPod); len(newIPErrs) > 0 {
|
||||||
allErrs = append(allErrs, newIPErrs...)
|
allErrs = append(allErrs, newIPErrs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if newIPErrs := validateHostIPs(newPod); len(newIPErrs) > 0 {
|
if newIPErrs := validateHostIPs(newPod, oldPod); len(newIPErrs) > 0 {
|
||||||
allErrs = append(allErrs, newIPErrs...)
|
allErrs = append(allErrs, newIPErrs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5860,7 +5874,7 @@ var supportedServiceIPFamilyPolicy = sets.New(
|
|||||||
core.IPFamilyPolicyRequireDualStack)
|
core.IPFamilyPolicyRequireDualStack)
|
||||||
|
|
||||||
// ValidateService tests if required fields/annotations of a Service are valid.
|
// ValidateService tests if required fields/annotations of a Service are valid.
|
||||||
func ValidateService(service *core.Service) field.ErrorList {
|
func ValidateService(service, oldService *core.Service) field.ErrorList {
|
||||||
metaPath := field.NewPath("metadata")
|
metaPath := field.NewPath("metadata")
|
||||||
allErrs := ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName, metaPath)
|
allErrs := ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName, metaPath)
|
||||||
|
|
||||||
@ -5935,15 +5949,24 @@ func ValidateService(service *core.Service) field.ErrorList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dualstack <-> ClusterIPs <-> ipfamilies
|
// dualstack <-> ClusterIPs <-> ipfamilies
|
||||||
allErrs = append(allErrs, ValidateServiceClusterIPsRelatedFields(service)...)
|
allErrs = append(allErrs, ValidateServiceClusterIPsRelatedFields(service, oldService)...)
|
||||||
|
|
||||||
|
// All new ExternalIPs must be valid and "non-special". (Existing ExternalIPs may
|
||||||
|
// have been validated against older rules, but if we allowed them before we can't
|
||||||
|
// reject them now.)
|
||||||
ipPath := specPath.Child("externalIPs")
|
ipPath := specPath.Child("externalIPs")
|
||||||
|
var existingExternalIPs []string
|
||||||
|
if oldService != nil {
|
||||||
|
existingExternalIPs = oldService.Spec.ExternalIPs // +k8s:verify-mutation:reason=clone
|
||||||
|
}
|
||||||
for i, ip := range service.Spec.ExternalIPs {
|
for i, ip := range service.Spec.ExternalIPs {
|
||||||
idxPath := ipPath.Index(i)
|
idxPath := ipPath.Index(i)
|
||||||
if errs := validation.IsValidIP(idxPath, ip); len(errs) != 0 {
|
if errs := IsValidIPForLegacyField(idxPath, ip, existingExternalIPs); len(errs) != 0 {
|
||||||
allErrs = append(allErrs, errs...)
|
allErrs = append(allErrs, errs...)
|
||||||
} else {
|
} else {
|
||||||
allErrs = append(allErrs, ValidateNonSpecialIP(ip, idxPath)...)
|
// For historical reasons, this uses ValidateEndpointIP even
|
||||||
|
// though that is not exactly the appropriate set of checks.
|
||||||
|
allErrs = append(allErrs, ValidateEndpointIP(ip, idxPath)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5995,18 +6018,27 @@ func ValidateService(service *core.Service) field.ErrorList {
|
|||||||
ports[key] = true
|
ports[key] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate SourceRanges field or annotation.
|
// Validate SourceRanges field or annotation. Existing invalid CIDR values do not
|
||||||
|
// need to be fixed. Note that even with the tighter CIDR validation we still
|
||||||
|
// allow excess whitespace, because that is effectively part of the API.
|
||||||
if len(service.Spec.LoadBalancerSourceRanges) > 0 {
|
if len(service.Spec.LoadBalancerSourceRanges) > 0 {
|
||||||
fieldPath := specPath.Child("LoadBalancerSourceRanges")
|
fieldPath := specPath.Child("LoadBalancerSourceRanges")
|
||||||
|
|
||||||
if service.Spec.Type != core.ServiceTypeLoadBalancer {
|
if service.Spec.Type != core.ServiceTypeLoadBalancer {
|
||||||
allErrs = append(allErrs, field.Forbidden(fieldPath, "may only be used when `type` is 'LoadBalancer'"))
|
allErrs = append(allErrs, field.Forbidden(fieldPath, "may only be used when `type` is 'LoadBalancer'"))
|
||||||
}
|
}
|
||||||
|
var existingSourceRanges []string
|
||||||
|
if oldService != nil {
|
||||||
|
existingSourceRanges = make([]string, len(oldService.Spec.LoadBalancerSourceRanges))
|
||||||
|
for i, value := range oldService.Spec.LoadBalancerSourceRanges {
|
||||||
|
existingSourceRanges[i] = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
for idx, value := range service.Spec.LoadBalancerSourceRanges {
|
for idx, value := range service.Spec.LoadBalancerSourceRanges {
|
||||||
// Note: due to a historical accident around transition from the
|
// Note: due to a historical accident around transition from the
|
||||||
// annotation value, these values are allowed to be space-padded.
|
// annotation value, these values are allowed to be space-padded.
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
allErrs = append(allErrs, validation.IsValidCIDR(fieldPath.Index(idx), value)...)
|
allErrs = append(allErrs, IsValidCIDRForLegacyField(fieldPath.Index(idx), value, existingSourceRanges)...)
|
||||||
}
|
}
|
||||||
} else if val, annotationSet := service.Annotations[core.AnnotationLoadBalancerSourceRangesKey]; annotationSet {
|
} else if val, annotationSet := service.Annotations[core.AnnotationLoadBalancerSourceRangesKey]; annotationSet {
|
||||||
fieldPath := field.NewPath("metadata", "annotations").Key(core.AnnotationLoadBalancerSourceRangesKey)
|
fieldPath := field.NewPath("metadata", "annotations").Key(core.AnnotationLoadBalancerSourceRangesKey)
|
||||||
@ -6014,12 +6046,14 @@ func ValidateService(service *core.Service) field.ErrorList {
|
|||||||
allErrs = append(allErrs, field.Forbidden(fieldPath, "may only be used when `type` is 'LoadBalancer'"))
|
allErrs = append(allErrs, field.Forbidden(fieldPath, "may only be used when `type` is 'LoadBalancer'"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val = strings.TrimSpace(val)
|
if oldService == nil || oldService.Annotations[core.AnnotationLoadBalancerSourceRangesKey] != val {
|
||||||
if val != "" {
|
val = strings.TrimSpace(val)
|
||||||
cidrs := strings.Split(val, ",")
|
if val != "" {
|
||||||
for _, value := range cidrs {
|
cidrs := strings.Split(val, ",")
|
||||||
value = strings.TrimSpace(value)
|
for _, value := range cidrs {
|
||||||
allErrs = append(allErrs, validation.IsValidCIDR(fieldPath, value)...)
|
value = strings.TrimSpace(value)
|
||||||
|
allErrs = append(allErrs, IsValidCIDRForLegacyField(fieldPath, value, nil)...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6179,7 +6213,7 @@ func validateServiceTrafficDistribution(service *core.Service) field.ErrorList {
|
|||||||
|
|
||||||
// ValidateServiceCreate validates Services as they are created.
|
// ValidateServiceCreate validates Services as they are created.
|
||||||
func ValidateServiceCreate(service *core.Service) field.ErrorList {
|
func ValidateServiceCreate(service *core.Service) field.ErrorList {
|
||||||
return ValidateService(service)
|
return ValidateService(service, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateServiceUpdate tests if required fields in the service are set during an update
|
// ValidateServiceUpdate tests if required fields in the service are set during an update
|
||||||
@ -6202,13 +6236,13 @@ func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
|
|||||||
|
|
||||||
allErrs = append(allErrs, validateServiceExternalTrafficFieldsUpdate(oldService, service)...)
|
allErrs = append(allErrs, validateServiceExternalTrafficFieldsUpdate(oldService, service)...)
|
||||||
|
|
||||||
return append(allErrs, ValidateService(service)...)
|
return append(allErrs, ValidateService(service, oldService)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateServiceStatusUpdate tests if required fields in the Service are set when updating status.
|
// ValidateServiceStatusUpdate tests if required fields in the Service are set when updating status.
|
||||||
func ValidateServiceStatusUpdate(service, oldService *core.Service) field.ErrorList {
|
func ValidateServiceStatusUpdate(service, oldService *core.Service) field.ErrorList {
|
||||||
allErrs := ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, field.NewPath("metadata"))
|
allErrs := ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, field.NewPath("metadata"))
|
||||||
allErrs = append(allErrs, ValidateLoadBalancerStatus(&service.Status.LoadBalancer, field.NewPath("status", "loadBalancer"), &service.Spec)...)
|
allErrs = append(allErrs, ValidateLoadBalancerStatus(&service.Status.LoadBalancer, &oldService.Status.LoadBalancer, field.NewPath("status", "loadBalancer"), &service.Spec)...)
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6403,7 +6437,7 @@ func ValidateNode(node *core.Node) field.ErrorList {
|
|||||||
|
|
||||||
// all PodCIDRs should be valid ones
|
// all PodCIDRs should be valid ones
|
||||||
for idx, value := range node.Spec.PodCIDRs {
|
for idx, value := range node.Spec.PodCIDRs {
|
||||||
allErrs = append(allErrs, validation.IsValidCIDR(podCIDRsField.Index(idx), value)...)
|
allErrs = append(allErrs, IsValidCIDRForLegacyField(podCIDRsField.Index(idx), value, nil)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if more than PodCIDR then it must be a dual-stack pair
|
// if more than PodCIDR then it must be a dual-stack pair
|
||||||
@ -7430,16 +7464,28 @@ func ValidateNamespaceFinalizeUpdate(newNamespace, oldNamespace *core.Namespace)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateEndpoints validates Endpoints on create and update.
|
// ValidateEndpoints validates Endpoints on create and update.
|
||||||
func ValidateEndpoints(endpoints *core.Endpoints) field.ErrorList {
|
func ValidateEndpoints(endpoints, oldEndpoints *core.Endpoints) field.ErrorList {
|
||||||
allErrs := ValidateObjectMeta(&endpoints.ObjectMeta, true, ValidateEndpointsName, field.NewPath("metadata"))
|
allErrs := ValidateObjectMeta(&endpoints.ObjectMeta, true, ValidateEndpointsName, field.NewPath("metadata"))
|
||||||
allErrs = append(allErrs, ValidateEndpointsSpecificAnnotations(endpoints.Annotations, field.NewPath("annotations"))...)
|
allErrs = append(allErrs, ValidateEndpointsSpecificAnnotations(endpoints.Annotations, field.NewPath("annotations"))...)
|
||||||
allErrs = append(allErrs, validateEndpointSubsets(endpoints.Subsets, field.NewPath("subsets"))...)
|
|
||||||
|
subsetErrs := validateEndpointSubsets(endpoints.Subsets, field.NewPath("subsets"))
|
||||||
|
if len(subsetErrs) != 0 {
|
||||||
|
// If this is an update, and Subsets was unchanged, then ignore the
|
||||||
|
// validation errors, since apparently older versions of Kubernetes
|
||||||
|
// considered the data valid. (We only check this after getting a
|
||||||
|
// validation error since Endpoints may be large and DeepEqual is slow.)
|
||||||
|
if oldEndpoints != nil && apiequality.Semantic.DeepEqual(oldEndpoints.Subsets, endpoints.Subsets) {
|
||||||
|
subsetErrs = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, subsetErrs...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateEndpointsCreate validates Endpoints on create.
|
// ValidateEndpointsCreate validates Endpoints on create.
|
||||||
func ValidateEndpointsCreate(endpoints *core.Endpoints) field.ErrorList {
|
func ValidateEndpointsCreate(endpoints *core.Endpoints) field.ErrorList {
|
||||||
return ValidateEndpoints(endpoints)
|
return ValidateEndpoints(endpoints, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateEndpointsUpdate validates Endpoints on update. NodeName changes are
|
// ValidateEndpointsUpdate validates Endpoints on update. NodeName changes are
|
||||||
@ -7448,7 +7494,7 @@ func ValidateEndpointsCreate(endpoints *core.Endpoints) field.ErrorList {
|
|||||||
// happens.
|
// happens.
|
||||||
func ValidateEndpointsUpdate(newEndpoints, oldEndpoints *core.Endpoints) field.ErrorList {
|
func ValidateEndpointsUpdate(newEndpoints, oldEndpoints *core.Endpoints) field.ErrorList {
|
||||||
allErrs := ValidateObjectMetaUpdate(&newEndpoints.ObjectMeta, &oldEndpoints.ObjectMeta, field.NewPath("metadata"))
|
allErrs := ValidateObjectMetaUpdate(&newEndpoints.ObjectMeta, &oldEndpoints.ObjectMeta, field.NewPath("metadata"))
|
||||||
allErrs = append(allErrs, ValidateEndpoints(newEndpoints)...)
|
allErrs = append(allErrs, ValidateEndpoints(newEndpoints, oldEndpoints)...)
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7479,7 +7525,7 @@ func validateEndpointSubsets(subsets []core.EndpointSubset, fldPath *field.Path)
|
|||||||
|
|
||||||
func validateEndpointAddress(address *core.EndpointAddress, fldPath *field.Path) field.ErrorList {
|
func validateEndpointAddress(address *core.EndpointAddress, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
allErrs = append(allErrs, validation.IsValidIP(fldPath.Child("ip"), address.IP)...)
|
allErrs = append(allErrs, IsValidIPForLegacyField(fldPath.Child("ip"), address.IP, nil)...)
|
||||||
if len(address.Hostname) > 0 {
|
if len(address.Hostname) > 0 {
|
||||||
allErrs = append(allErrs, ValidateDNS1123Label(address.Hostname, fldPath.Child("hostname"))...)
|
allErrs = append(allErrs, ValidateDNS1123Label(address.Hostname, fldPath.Child("hostname"))...)
|
||||||
}
|
}
|
||||||
@ -7489,19 +7535,21 @@ func validateEndpointAddress(address *core.EndpointAddress, fldPath *field.Path)
|
|||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeName"), *address.NodeName, msg).WithOrigin("format=dns-label"))
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeName"), *address.NodeName, msg).WithOrigin("format=dns-label"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allErrs = append(allErrs, ValidateNonSpecialIP(address.IP, fldPath.Child("ip"))...)
|
allErrs = append(allErrs, ValidateEndpointIP(address.IP, fldPath.Child("ip"))...)
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateNonSpecialIP is used to validate Endpoints, EndpointSlices, and
|
// ValidateEndpointIP is used to validate Endpoints and EndpointSlice addresses, and also
|
||||||
// external IPs. Specifically, this disallows unspecified and loopback addresses
|
// (for historical reasons) external IPs. It disallows certain address types that don't
|
||||||
// are nonsensical and link-local addresses tend to be used for node-centric
|
// make sense in those contexts. Note that this function is _almost_, but not exactly,
|
||||||
// purposes (e.g. metadata service).
|
// equivalent to net.IP.IsGlobalUnicast(). (Unlike IsGlobalUnicast, it allows global
|
||||||
|
// multicast IPs, which is probably a bug.)
|
||||||
//
|
//
|
||||||
// IPv6 references
|
// This function should not be used for new validations; the exact set of IPs that do and
|
||||||
// - https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
|
// don't make sense in a particular field is context-dependent (e.g., localhost makes
|
||||||
// - https://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml
|
// sense in some places; unspecified IPs make sense in fields that are used as bind
|
||||||
func ValidateNonSpecialIP(ipAddress string, fldPath *field.Path) field.ErrorList {
|
// addresses rather than destination addresses).
|
||||||
|
func ValidateEndpointIP(ipAddress string, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
ip := netutils.ParseIPSloppy(ipAddress)
|
ip := netutils.ParseIPSloppy(ipAddress)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
@ -7520,7 +7568,7 @@ func ValidateNonSpecialIP(ipAddress string, fldPath *field.Path) field.ErrorList
|
|||||||
if ip.IsLinkLocalMulticast() {
|
if ip.IsLinkLocalMulticast() {
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the link-local multicast range (224.0.0.0/24, ff02::/10)"))
|
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the link-local multicast range (224.0.0.0/24, ff02::/10)"))
|
||||||
}
|
}
|
||||||
return allErrs.WithOrigin("format=non-special-ip")
|
return allErrs.WithOrigin("format=endpoint-ip")
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpointPort(port *core.EndpointPort, requireName bool, fldPath *field.Path) field.ErrorList {
|
func validateEndpointPort(port *core.EndpointPort, requireName bool, fldPath *field.Path) field.ErrorList {
|
||||||
@ -7840,16 +7888,25 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ValidateLoadBalancerStatus validates required fields on a LoadBalancerStatus
|
// ValidateLoadBalancerStatus validates required fields on a LoadBalancerStatus
|
||||||
func ValidateLoadBalancerStatus(status *core.LoadBalancerStatus, fldPath *field.Path, spec *core.ServiceSpec) field.ErrorList {
|
func ValidateLoadBalancerStatus(status, oldStatus *core.LoadBalancerStatus, fldPath *field.Path, spec *core.ServiceSpec) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
ingrPath := fldPath.Child("ingress")
|
ingrPath := fldPath.Child("ingress")
|
||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.AllowServiceLBStatusOnNonLB) && spec.Type != core.ServiceTypeLoadBalancer && len(status.Ingress) != 0 {
|
if !utilfeature.DefaultFeatureGate.Enabled(features.AllowServiceLBStatusOnNonLB) && spec.Type != core.ServiceTypeLoadBalancer && len(status.Ingress) != 0 {
|
||||||
allErrs = append(allErrs, field.Forbidden(ingrPath, "may only be used when `spec.type` is 'LoadBalancer'"))
|
allErrs = append(allErrs, field.Forbidden(ingrPath, "may only be used when `spec.type` is 'LoadBalancer'"))
|
||||||
} else {
|
} else {
|
||||||
|
var existingIngressIPs []string
|
||||||
|
if oldStatus != nil {
|
||||||
|
existingIngressIPs = make([]string, 0, len(oldStatus.Ingress))
|
||||||
|
for _, ingress := range oldStatus.Ingress {
|
||||||
|
if len(ingress.IP) > 0 {
|
||||||
|
existingIngressIPs = append(existingIngressIPs, ingress.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, ingress := range status.Ingress {
|
for i, ingress := range status.Ingress {
|
||||||
idxPath := ingrPath.Index(i)
|
idxPath := ingrPath.Index(i)
|
||||||
if len(ingress.IP) > 0 {
|
if len(ingress.IP) > 0 {
|
||||||
allErrs = append(allErrs, validation.IsValidIP(idxPath.Child("ip"), ingress.IP)...)
|
allErrs = append(allErrs, IsValidIPForLegacyField(idxPath.Child("ip"), ingress.IP, existingIngressIPs)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(features.LoadBalancerIPMode) && ingress.IPMode == nil {
|
if utilfeature.DefaultFeatureGate.Enabled(features.LoadBalancerIPMode) && ingress.IPMode == nil {
|
||||||
@ -8116,7 +8173,7 @@ func validateLabelKeys(fldPath *field.Path, labelKeys []string, labelSelector *m
|
|||||||
// ValidateServiceClusterIPsRelatedFields validates .spec.ClusterIPs,,
|
// ValidateServiceClusterIPsRelatedFields validates .spec.ClusterIPs,,
|
||||||
// .spec.IPFamilies, .spec.ipFamilyPolicy. This is exported because it is used
|
// .spec.IPFamilies, .spec.ipFamilyPolicy. This is exported because it is used
|
||||||
// during IP init and allocation.
|
// during IP init and allocation.
|
||||||
func ValidateServiceClusterIPsRelatedFields(service *core.Service) field.ErrorList {
|
func ValidateServiceClusterIPsRelatedFields(service, oldService *core.Service) field.ErrorList {
|
||||||
// ClusterIP, ClusterIPs, IPFamilyPolicy and IPFamilies are validated prior (all must be unset) for ExternalName service
|
// ClusterIP, ClusterIPs, IPFamilyPolicy and IPFamilies are validated prior (all must be unset) for ExternalName service
|
||||||
if service.Spec.Type == core.ServiceTypeExternalName {
|
if service.Spec.Type == core.ServiceTypeExternalName {
|
||||||
return field.ErrorList{}
|
return field.ErrorList{}
|
||||||
@ -8170,6 +8227,11 @@ func ValidateServiceClusterIPsRelatedFields(service *core.Service) field.ErrorLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingClusterIPs []string
|
||||||
|
if oldService != nil {
|
||||||
|
existingClusterIPs = oldService.Spec.ClusterIPs // +k8s:verify-mutation:reason=clone
|
||||||
|
}
|
||||||
|
|
||||||
// clusterIPs stand alone validation
|
// clusterIPs stand alone validation
|
||||||
// valid ips with None and empty string handling
|
// valid ips with None and empty string handling
|
||||||
// duplication check is done as part of DualStackvalidation below
|
// duplication check is done as part of DualStackvalidation below
|
||||||
@ -8183,8 +8245,8 @@ func ValidateServiceClusterIPsRelatedFields(service *core.Service) field.ErrorLi
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// is it valid ip?
|
// is it a valid ip? (or was it at least previously considered valid?)
|
||||||
errorMessages := validation.IsValidIP(clusterIPsField.Index(i), clusterIP)
|
errorMessages := IsValidIPForLegacyField(clusterIPsField.Index(i), clusterIP, existingClusterIPs)
|
||||||
hasInvalidIPs = (len(errorMessages) != 0) || hasInvalidIPs
|
hasInvalidIPs = (len(errorMessages) != 0) || hasInvalidIPs
|
||||||
allErrs = append(allErrs, errorMessages...)
|
allErrs = append(allErrs, errorMessages...)
|
||||||
}
|
}
|
||||||
@ -8699,3 +8761,17 @@ func isRestartableInitContainer(initContainer *core.Container) bool {
|
|||||||
}
|
}
|
||||||
return *initContainer.RestartPolicy == core.ContainerRestartPolicyAlways
|
return *initContainer.RestartPolicy == core.ContainerRestartPolicyAlways
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValidIPForLegacyField is a wrapper around validation.IsValidIPForLegacyField that
|
||||||
|
// handles setting strictValidation correctly. This is only for fields that use legacy IP
|
||||||
|
// address validation; use validation.IsValidIP for new fields.
|
||||||
|
func IsValidIPForLegacyField(fldPath *field.Path, value string, validOldIPs []string) field.ErrorList {
|
||||||
|
return validation.IsValidIPForLegacyField(fldPath, value, utilfeature.DefaultFeatureGate.Enabled(features.StrictIPCIDRValidation), validOldIPs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidCIDRForLegacyField is a wrapper around validation.IsValidCIDRForLegacyField that
|
||||||
|
// handles setting strictValidation correctly. This is only for fields that use legacy CIDR
|
||||||
|
// value validation; use validation.IsValidCIDR for new fields.
|
||||||
|
func IsValidCIDRForLegacyField(fldPath *field.Path, value string, validOldCIDRs []string) field.ErrorList {
|
||||||
|
return validation.IsValidCIDRForLegacyField(fldPath, value, utilfeature.DefaultFeatureGate.Enabled(features.StrictIPCIDRValidation), validOldCIDRs)
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
@ -28,6 +29,7 @@ import (
|
|||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||||
"k8s.io/kubernetes/pkg/apis/discovery"
|
"k8s.io/kubernetes/pkg/apis/discovery"
|
||||||
|
netutils "k8s.io/utils/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -54,23 +56,34 @@ var (
|
|||||||
var ValidateEndpointSliceName = apimachineryvalidation.NameIsDNSSubdomain
|
var ValidateEndpointSliceName = apimachineryvalidation.NameIsDNSSubdomain
|
||||||
|
|
||||||
// ValidateEndpointSlice validates an EndpointSlice.
|
// ValidateEndpointSlice validates an EndpointSlice.
|
||||||
func ValidateEndpointSlice(endpointSlice *discovery.EndpointSlice) field.ErrorList {
|
func ValidateEndpointSlice(endpointSlice, oldEndpointSlice *discovery.EndpointSlice) field.ErrorList {
|
||||||
allErrs := apivalidation.ValidateObjectMeta(&endpointSlice.ObjectMeta, true, ValidateEndpointSliceName, field.NewPath("metadata"))
|
allErrs := apivalidation.ValidateObjectMeta(&endpointSlice.ObjectMeta, true, ValidateEndpointSliceName, field.NewPath("metadata"))
|
||||||
allErrs = append(allErrs, validateAddressType(endpointSlice.AddressType)...)
|
allErrs = append(allErrs, validateAddressType(endpointSlice.AddressType)...)
|
||||||
allErrs = append(allErrs, validateEndpoints(endpointSlice.Endpoints, endpointSlice.AddressType, field.NewPath("endpoints"))...)
|
|
||||||
allErrs = append(allErrs, validatePorts(endpointSlice.Ports, field.NewPath("ports"))...)
|
allErrs = append(allErrs, validatePorts(endpointSlice.Ports, field.NewPath("ports"))...)
|
||||||
|
|
||||||
|
endpointsErrs := validateEndpoints(endpointSlice.Endpoints, endpointSlice.AddressType, field.NewPath("endpoints"))
|
||||||
|
if len(endpointsErrs) != 0 {
|
||||||
|
// If this is an update, and Endpoints was unchanged, then ignore the
|
||||||
|
// validation errors, since apparently older versions of Kubernetes
|
||||||
|
// considered the data valid. (We only check this after getting a
|
||||||
|
// validation error since Endpoints may be large and DeepEqual is slow.)
|
||||||
|
if oldEndpointSlice != nil && apiequality.Semantic.DeepEqual(oldEndpointSlice.Endpoints, endpointSlice.Endpoints) {
|
||||||
|
endpointsErrs = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, endpointsErrs...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateEndpointSliceCreate validates an EndpointSlice when it is created.
|
// ValidateEndpointSliceCreate validates an EndpointSlice when it is created.
|
||||||
func ValidateEndpointSliceCreate(endpointSlice *discovery.EndpointSlice) field.ErrorList {
|
func ValidateEndpointSliceCreate(endpointSlice *discovery.EndpointSlice) field.ErrorList {
|
||||||
return ValidateEndpointSlice(endpointSlice)
|
return ValidateEndpointSlice(endpointSlice, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateEndpointSliceUpdate validates an EndpointSlice when it is updated.
|
// ValidateEndpointSliceUpdate validates an EndpointSlice when it is updated.
|
||||||
func ValidateEndpointSliceUpdate(newEndpointSlice, oldEndpointSlice *discovery.EndpointSlice) field.ErrorList {
|
func ValidateEndpointSliceUpdate(newEndpointSlice, oldEndpointSlice *discovery.EndpointSlice) field.ErrorList {
|
||||||
allErrs := ValidateEndpointSlice(newEndpointSlice)
|
allErrs := ValidateEndpointSlice(newEndpointSlice, oldEndpointSlice)
|
||||||
allErrs = append(allErrs, apivalidation.ValidateImmutableField(newEndpointSlice.AddressType, oldEndpointSlice.AddressType, field.NewPath("addressType"))...)
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(newEndpointSlice.AddressType, oldEndpointSlice.AddressType, field.NewPath("addressType"))...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
@ -99,11 +112,25 @@ func validateEndpoints(endpoints []discovery.Endpoint, addrType discovery.Addres
|
|||||||
// and do not get validated.
|
// and do not get validated.
|
||||||
switch addrType {
|
switch addrType {
|
||||||
case discovery.AddressTypeIPv4:
|
case discovery.AddressTypeIPv4:
|
||||||
allErrs = append(allErrs, validation.IsValidIPv4Address(addressPath.Index(i), address)...)
|
ipErrs := apivalidation.IsValidIPForLegacyField(addressPath.Index(i), address, nil)
|
||||||
allErrs = append(allErrs, apivalidation.ValidateNonSpecialIP(address, addressPath.Index(i))...)
|
if len(ipErrs) > 0 {
|
||||||
|
allErrs = append(allErrs, ipErrs...)
|
||||||
|
} else {
|
||||||
|
if !netutils.IsIPv4String(address) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(addressPath, address, "must be an IPv4 address"))
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidateEndpointIP(address, addressPath.Index(i))...)
|
||||||
|
}
|
||||||
case discovery.AddressTypeIPv6:
|
case discovery.AddressTypeIPv6:
|
||||||
allErrs = append(allErrs, validation.IsValidIPv6Address(addressPath.Index(i), address)...)
|
ipErrs := validation.IsValidIP(addressPath.Index(i), address)
|
||||||
allErrs = append(allErrs, apivalidation.ValidateNonSpecialIP(address, addressPath.Index(i))...)
|
if len(ipErrs) > 0 {
|
||||||
|
allErrs = append(allErrs, ipErrs...)
|
||||||
|
} else {
|
||||||
|
if !netutils.IsIPv6String(address) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(addressPath, address, "must be an IPv6 address"))
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidateEndpointIP(address, addressPath.Index(i))...)
|
||||||
|
}
|
||||||
case discovery.AddressTypeFQDN:
|
case discovery.AddressTypeFQDN:
|
||||||
allErrs = append(allErrs, validation.IsFullyQualifiedDomainName(addressPath.Index(i), address)...)
|
allErrs = append(allErrs, validation.IsFullyQualifiedDomainName(addressPath.Index(i), address)...)
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,11 @@ import (
|
|||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/apis/discovery"
|
"k8s.io/kubernetes/pkg/apis/discovery"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,6 +39,7 @@ func TestValidateEndpointSlice(t *testing.T) {
|
|||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
expectedErrors int
|
expectedErrors int
|
||||||
|
legacyIPs bool
|
||||||
endpointSlice *discovery.EndpointSlice
|
endpointSlice *discovery.EndpointSlice
|
||||||
}{
|
}{
|
||||||
"good-slice": {
|
"good-slice": {
|
||||||
@ -235,6 +239,22 @@ func TestValidateEndpointSlice(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"legacy-ip-with-legacy-validation": {
|
||||||
|
expectedErrors: 0,
|
||||||
|
legacyIPs: true,
|
||||||
|
endpointSlice: &discovery.EndpointSlice{
|
||||||
|
ObjectMeta: standardMeta,
|
||||||
|
AddressType: discovery.AddressTypeIPv4,
|
||||||
|
Ports: []discovery.EndpointPort{{
|
||||||
|
Name: ptr.To("http"),
|
||||||
|
Protocol: ptr.To(api.ProtocolTCP),
|
||||||
|
}},
|
||||||
|
Endpoints: []discovery.Endpoint{{
|
||||||
|
Addresses: []string{"012.034.056.078"},
|
||||||
|
Hostname: ptr.To("valid-123"),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// expected failures
|
// expected failures
|
||||||
"duplicate-port-name": {
|
"duplicate-port-name": {
|
||||||
@ -408,7 +428,7 @@ func TestValidateEndpointSlice(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"bad-ip": {
|
"bad-ip": {
|
||||||
expectedErrors: 2,
|
expectedErrors: 1,
|
||||||
endpointSlice: &discovery.EndpointSlice{
|
endpointSlice: &discovery.EndpointSlice{
|
||||||
ObjectMeta: standardMeta,
|
ObjectMeta: standardMeta,
|
||||||
AddressType: discovery.AddressTypeIPv4,
|
AddressType: discovery.AddressTypeIPv4,
|
||||||
@ -422,8 +442,23 @@ func TestValidateEndpointSlice(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"legacy-ip-with-strict-validation": {
|
||||||
|
expectedErrors: 1,
|
||||||
|
endpointSlice: &discovery.EndpointSlice{
|
||||||
|
ObjectMeta: standardMeta,
|
||||||
|
AddressType: discovery.AddressTypeIPv4,
|
||||||
|
Ports: []discovery.EndpointPort{{
|
||||||
|
Name: ptr.To("http"),
|
||||||
|
Protocol: ptr.To(api.ProtocolTCP),
|
||||||
|
}},
|
||||||
|
Endpoints: []discovery.Endpoint{{
|
||||||
|
Addresses: []string{"012.034.056.078"},
|
||||||
|
Hostname: ptr.To("valid-123"),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
"bad-ipv4": {
|
"bad-ipv4": {
|
||||||
expectedErrors: 3,
|
expectedErrors: 2,
|
||||||
endpointSlice: &discovery.EndpointSlice{
|
endpointSlice: &discovery.EndpointSlice{
|
||||||
ObjectMeta: standardMeta,
|
ObjectMeta: standardMeta,
|
||||||
AddressType: discovery.AddressTypeIPv4,
|
AddressType: discovery.AddressTypeIPv4,
|
||||||
@ -438,7 +473,7 @@ func TestValidateEndpointSlice(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"bad-ipv6": {
|
"bad-ipv6": {
|
||||||
expectedErrors: 4,
|
expectedErrors: 2,
|
||||||
endpointSlice: &discovery.EndpointSlice{
|
endpointSlice: &discovery.EndpointSlice{
|
||||||
ObjectMeta: standardMeta,
|
ObjectMeta: standardMeta,
|
||||||
AddressType: discovery.AddressTypeIPv6,
|
AddressType: discovery.AddressTypeIPv6,
|
||||||
@ -601,7 +636,8 @@ func TestValidateEndpointSlice(t *testing.T) {
|
|||||||
|
|
||||||
for name, testCase := range testCases {
|
for name, testCase := range testCases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
errs := ValidateEndpointSlice(testCase.endpointSlice)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, !testCase.legacyIPs)
|
||||||
|
errs := ValidateEndpointSlice(testCase.endpointSlice, nil)
|
||||||
if len(errs) != testCase.expectedErrors {
|
if len(errs) != testCase.expectedErrors {
|
||||||
t.Errorf("Expected %d errors, got %d errors: %v", testCase.expectedErrors, len(errs), errs)
|
t.Errorf("Expected %d errors, got %d errors: %v", testCase.expectedErrors, len(errs), errs)
|
||||||
}
|
}
|
||||||
@ -701,6 +737,7 @@ func TestValidateEndpointSliceCreate(t *testing.T) {
|
|||||||
|
|
||||||
for name, testCase := range testCases {
|
for name, testCase := range testCases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, true)
|
||||||
errs := ValidateEndpointSliceCreate(testCase.endpointSlice)
|
errs := ValidateEndpointSliceCreate(testCase.endpointSlice)
|
||||||
if len(errs) != testCase.expectedErrors {
|
if len(errs) != testCase.expectedErrors {
|
||||||
t.Errorf("Expected %d errors, got %d errors: %v", testCase.expectedErrors, len(errs), errs)
|
t.Errorf("Expected %d errors, got %d errors: %v", testCase.expectedErrors, len(errs), errs)
|
||||||
@ -729,6 +766,23 @@ func TestValidateEndpointSliceUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedErrors: 0,
|
expectedErrors: 0,
|
||||||
},
|
},
|
||||||
|
"unchanged IPs are not revalidated": {
|
||||||
|
oldEndpointSlice: &discovery.EndpointSlice{
|
||||||
|
ObjectMeta: standardMeta,
|
||||||
|
AddressType: discovery.AddressTypeIPv4,
|
||||||
|
Endpoints: []discovery.Endpoint{{
|
||||||
|
Addresses: []string{"10.1.2.04"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
newEndpointSlice: &discovery.EndpointSlice{
|
||||||
|
ObjectMeta: standardMeta,
|
||||||
|
AddressType: discovery.AddressTypeIPv4,
|
||||||
|
Endpoints: []discovery.Endpoint{{
|
||||||
|
Addresses: []string{"10.1.2.04"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
expectedErrors: 0,
|
||||||
|
},
|
||||||
|
|
||||||
// expected errors
|
// expected errors
|
||||||
"invalid node name set": {
|
"invalid node name set": {
|
||||||
@ -787,10 +841,28 @@ func TestValidateEndpointSliceUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedErrors: 1,
|
expectedErrors: 1,
|
||||||
},
|
},
|
||||||
|
"changed IPs are revalidated": {
|
||||||
|
oldEndpointSlice: &discovery.EndpointSlice{
|
||||||
|
ObjectMeta: standardMeta,
|
||||||
|
AddressType: discovery.AddressTypeIPv4,
|
||||||
|
Endpoints: []discovery.Endpoint{{
|
||||||
|
Addresses: []string{"10.1.2.3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
newEndpointSlice: &discovery.EndpointSlice{
|
||||||
|
ObjectMeta: standardMeta,
|
||||||
|
AddressType: discovery.AddressTypeIPv4,
|
||||||
|
Endpoints: []discovery.Endpoint{{
|
||||||
|
Addresses: []string{"10.1.2.03"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
expectedErrors: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
for name, testCase := range testCases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, true)
|
||||||
errs := ValidateEndpointSliceUpdate(testCase.newEndpointSlice, testCase.oldEndpointSlice)
|
errs := ValidateEndpointSliceUpdate(testCase.newEndpointSlice, testCase.oldEndpointSlice)
|
||||||
if len(errs) != testCase.expectedErrors {
|
if len(errs) != testCase.expectedErrors {
|
||||||
t.Errorf("Expected %d errors, got %d errors: %v", testCase.expectedErrors, len(errs), errs)
|
t.Errorf("Expected %d errors, got %d errors: %v", testCase.expectedErrors, len(errs), errs)
|
||||||
|
@ -18,7 +18,6 @@ package validation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
@ -57,6 +56,7 @@ var (
|
|||||||
|
|
||||||
type NetworkPolicyValidationOptions struct {
|
type NetworkPolicyValidationOptions struct {
|
||||||
AllowInvalidLabelValueInSelector bool
|
AllowInvalidLabelValueInSelector bool
|
||||||
|
AllowCIDRsEvenIfInvalid []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateNetworkPolicyName can be used to check whether the given networkpolicy
|
// ValidateNetworkPolicyName can be used to check whether the given networkpolicy
|
||||||
@ -119,7 +119,7 @@ func ValidateNetworkPolicyPeer(peer *networking.NetworkPolicyPeer, opts NetworkP
|
|||||||
}
|
}
|
||||||
if peer.IPBlock != nil {
|
if peer.IPBlock != nil {
|
||||||
numPeers++
|
numPeers++
|
||||||
allErrs = append(allErrs, ValidateIPBlock(peer.IPBlock, peerPath.Child("ipBlock"))...)
|
allErrs = append(allErrs, ValidateIPBlock(peer.IPBlock, peerPath.Child("ipBlock"), opts)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if numPeers == 0 {
|
if numPeers == 0 {
|
||||||
@ -207,20 +207,47 @@ func ValidationOptionsForNetworking(new, old *networking.NetworkPolicy) NetworkP
|
|||||||
|
|
||||||
// ValidateNetworkPolicyUpdate tests if an update to a NetworkPolicy is valid.
|
// ValidateNetworkPolicyUpdate tests if an update to a NetworkPolicy is valid.
|
||||||
func ValidateNetworkPolicyUpdate(update, old *networking.NetworkPolicy, opts NetworkPolicyValidationOptions) field.ErrorList {
|
func ValidateNetworkPolicyUpdate(update, old *networking.NetworkPolicy, opts NetworkPolicyValidationOptions) field.ErrorList {
|
||||||
|
// Record all existing CIDRs in the policy; see ValidateIPBlock.
|
||||||
|
var existingCIDRs []string
|
||||||
|
for _, ingress := range old.Spec.Ingress {
|
||||||
|
for _, peer := range ingress.From {
|
||||||
|
if peer.IPBlock != nil {
|
||||||
|
existingCIDRs = append(existingCIDRs, peer.IPBlock.CIDR)
|
||||||
|
existingCIDRs = append(existingCIDRs, peer.IPBlock.Except...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, egress := range old.Spec.Egress {
|
||||||
|
for _, peer := range egress.To {
|
||||||
|
if peer.IPBlock != nil {
|
||||||
|
existingCIDRs = append(existingCIDRs, peer.IPBlock.CIDR)
|
||||||
|
existingCIDRs = append(existingCIDRs, peer.IPBlock.Except...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts.AllowCIDRsEvenIfInvalid = existingCIDRs
|
||||||
|
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata"))...)
|
allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata"))...)
|
||||||
allErrs = append(allErrs, ValidateNetworkPolicySpec(&update.Spec, opts, field.NewPath("spec"))...)
|
allErrs = append(allErrs, ValidateNetworkPolicySpec(&update.Spec, opts, field.NewPath("spec"))...)
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateIPBlock validates a cidr and the except fields of an IpBlock NetworkPolicyPeer
|
// ValidateIPBlock validates a cidr and the except fields of an IPBlock NetworkPolicyPeer.
|
||||||
func ValidateIPBlock(ipb *networking.IPBlock, fldPath *field.Path) field.ErrorList {
|
//
|
||||||
|
// If a pre-existing CIDR is invalid/insecure, but it is not being changed by this update,
|
||||||
|
// then we have to continue allowing it. But since the user may have changed the policy in
|
||||||
|
// arbitrary ways (adding/removing rules, adding/removing peers, adding/removing
|
||||||
|
// ipBlock.except values, etc), we can't reliably determine whether a CIDR value we see
|
||||||
|
// here is a new value or a pre-existing one. So we just allow any CIDR value that
|
||||||
|
// appeared in the old NetworkPolicy to be used here without revalidation.
|
||||||
|
func ValidateIPBlock(ipb *networking.IPBlock, fldPath *field.Path, opts NetworkPolicyValidationOptions) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
if ipb.CIDR == "" {
|
if ipb.CIDR == "" {
|
||||||
allErrs = append(allErrs, field.Required(fldPath.Child("cidr"), ""))
|
allErrs = append(allErrs, field.Required(fldPath.Child("cidr"), ""))
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
allErrs = append(allErrs, validation.IsValidCIDR(fldPath.Child("cidr"), ipb.CIDR)...)
|
allErrs = append(allErrs, apivalidation.IsValidCIDRForLegacyField(fldPath.Child("cidr"), ipb.CIDR, opts.AllowCIDRsEvenIfInvalid)...)
|
||||||
_, cidrIPNet, err := netutils.ParseCIDRSloppy(ipb.CIDR)
|
_, cidrIPNet, err := netutils.ParseCIDRSloppy(ipb.CIDR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Implies validation would have failed so we already added errors for it.
|
// Implies validation would have failed so we already added errors for it.
|
||||||
@ -229,7 +256,7 @@ func ValidateIPBlock(ipb *networking.IPBlock, fldPath *field.Path) field.ErrorLi
|
|||||||
|
|
||||||
for i, exceptCIDRStr := range ipb.Except {
|
for i, exceptCIDRStr := range ipb.Except {
|
||||||
exceptPath := fldPath.Child("except").Index(i)
|
exceptPath := fldPath.Child("except").Index(i)
|
||||||
allErrs = append(allErrs, validation.IsValidCIDR(exceptPath, exceptCIDRStr)...)
|
allErrs = append(allErrs, apivalidation.IsValidCIDRForLegacyField(exceptPath, exceptCIDRStr, opts.AllowCIDRsEvenIfInvalid)...)
|
||||||
_, exceptCIDR, err := netutils.ParseCIDRSloppy(exceptCIDRStr)
|
_, exceptCIDR, err := netutils.ParseCIDRSloppy(exceptCIDRStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Implies validation would have failed so we already added errors for it.
|
// Implies validation would have failed so we already added errors for it.
|
||||||
@ -347,17 +374,28 @@ func ValidateIngressSpec(spec *networking.IngressSpec, fldPath *field.Path, opts
|
|||||||
// ValidateIngressStatusUpdate tests if required fields in the Ingress are set when updating status.
|
// ValidateIngressStatusUpdate tests if required fields in the Ingress are set when updating status.
|
||||||
func ValidateIngressStatusUpdate(ingress, oldIngress *networking.Ingress) field.ErrorList {
|
func ValidateIngressStatusUpdate(ingress, oldIngress *networking.Ingress) field.ErrorList {
|
||||||
allErrs := apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta, field.NewPath("metadata"))
|
allErrs := apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta, field.NewPath("metadata"))
|
||||||
allErrs = append(allErrs, ValidateIngressLoadBalancerStatus(&ingress.Status.LoadBalancer, field.NewPath("status", "loadBalancer"))...)
|
allErrs = append(allErrs, ValidateIngressLoadBalancerStatus(&ingress.Status.LoadBalancer, &oldIngress.Status.LoadBalancer, field.NewPath("status", "loadBalancer"))...)
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateIngressLoadBalancerStatus validates required fields on an IngressLoadBalancerStatus
|
// ValidateIngressLoadBalancerStatus validates required fields on an IngressLoadBalancerStatus
|
||||||
func ValidateIngressLoadBalancerStatus(status *networking.IngressLoadBalancerStatus, fldPath *field.Path) field.ErrorList {
|
func ValidateIngressLoadBalancerStatus(status, oldStatus *networking.IngressLoadBalancerStatus, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
|
var existingIngressIPs []string
|
||||||
|
if oldStatus != nil {
|
||||||
|
existingIngressIPs = make([]string, 0, len(oldStatus.Ingress))
|
||||||
|
for _, ingress := range oldStatus.Ingress {
|
||||||
|
if len(ingress.IP) > 0 {
|
||||||
|
existingIngressIPs = append(existingIngressIPs, ingress.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i, ingress := range status.Ingress {
|
for i, ingress := range status.Ingress {
|
||||||
idxPath := fldPath.Child("ingress").Index(i)
|
idxPath := fldPath.Child("ingress").Index(i)
|
||||||
if len(ingress.IP) > 0 {
|
if len(ingress.IP) > 0 {
|
||||||
allErrs = append(allErrs, validation.IsValidIP(idxPath.Child("ip"), ingress.IP)...)
|
allErrs = append(allErrs, apivalidation.IsValidIPForLegacyField(idxPath.Child("ip"), ingress.IP, existingIngressIPs)...)
|
||||||
}
|
}
|
||||||
if len(ingress.Hostname) > 0 {
|
if len(ingress.Hostname) > 0 {
|
||||||
for _, msg := range validation.IsDNS1123Subdomain(ingress.Hostname) {
|
for _, msg := range validation.IsDNS1123Subdomain(ingress.Hostname) {
|
||||||
@ -653,12 +691,12 @@ func allowInvalidWildcardHostRule(oldIngress *networking.Ingress) bool {
|
|||||||
// IPAddress does not support generating names, prefix is not considered.
|
// IPAddress does not support generating names, prefix is not considered.
|
||||||
func ValidateIPAddressName(name string, prefix bool) []string {
|
func ValidateIPAddressName(name string, prefix bool) []string {
|
||||||
var errs []string
|
var errs []string
|
||||||
ip, err := netip.ParseAddr(name)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, err.Error())
|
|
||||||
} else if ip.String() != name {
|
|
||||||
errs = append(errs, "must be a canonical format IP address")
|
|
||||||
|
|
||||||
|
allErrs := validation.IsValidIP(&field.Path{}, name)
|
||||||
|
// Need to unconvert the field.Error from IsValidIP back to a string so our caller
|
||||||
|
// can convert it back to a field.Error!
|
||||||
|
for _, err := range allErrs {
|
||||||
|
errs = append(errs, err.Detail)
|
||||||
}
|
}
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
@ -748,28 +786,12 @@ func ValidateServiceCIDR(cidrConfig *networking.ServiceCIDR) field.ErrorList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, cidr := range cidrConfig.Spec.CIDRs {
|
for i, cidr := range cidrConfig.Spec.CIDRs {
|
||||||
allErrs = append(allErrs, validateCIDR(cidr, fieldPath.Index(i))...)
|
allErrs = append(allErrs, validation.IsValidCIDR(fieldPath.Index(i), cidr)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCIDR(cidr string, fldPath *field.Path) field.ErrorList {
|
|
||||||
allErrs := field.ErrorList{}
|
|
||||||
prefix, err := netip.ParsePrefix(cidr)
|
|
||||||
if err != nil {
|
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath, cidr, err.Error()))
|
|
||||||
} else {
|
|
||||||
if prefix.Addr() != prefix.Masked().Addr() {
|
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath, cidr, "wrong CIDR format, IP doesn't match network IP address"))
|
|
||||||
}
|
|
||||||
if prefix.String() != cidr {
|
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath, cidr, "CIDR not in RFC 5952 canonical format"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allErrs
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateServiceCIDRUpdate tests if an update to a ServiceCIDR is valid.
|
// ValidateServiceCIDRUpdate tests if an update to a ServiceCIDR is valid.
|
||||||
func ValidateServiceCIDRUpdate(update, old *networking.ServiceCIDR) field.ErrorList {
|
func ValidateServiceCIDRUpdate(update, old *networking.ServiceCIDR) field.ErrorList {
|
||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
|
@ -25,8 +25,11 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/apis/networking"
|
"k8s.io/kubernetes/pkg/apis/networking"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
utilpointer "k8s.io/utils/pointer"
|
utilpointer "k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -248,11 +251,27 @@ func TestValidateNetworkPolicy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Success cases are expected to pass validation.
|
// Success cases are expected to pass validation.
|
||||||
|
for _, v := range successCases {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, true)
|
||||||
|
if errs := ValidateNetworkPolicy(v, NetworkPolicyValidationOptions{AllowInvalidLabelValueInSelector: true}); len(errs) != 0 {
|
||||||
|
t.Errorf("Expected success, got %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range successCases {
|
legacyValidationCases := []*networking.NetworkPolicy{
|
||||||
if errs := ValidateNetworkPolicy(v, NetworkPolicyValidationOptions{AllowInvalidLabelValueInSelector: true}); len(errs) != 0 {
|
makeNetworkPolicyCustom(setIngressFromIPBlockIPV4, func(networkPolicy *networking.NetworkPolicy) {
|
||||||
t.Errorf("Expected success for the success validation test number %d, got %v", k, errs)
|
networkPolicy.Spec.Ingress[0].From[0].IPBlock.CIDR = "001.002.003.000/0"
|
||||||
}
|
}),
|
||||||
|
}
|
||||||
|
for _, v := range legacyValidationCases {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, false)
|
||||||
|
if errs := ValidateNetworkPolicy(v, NetworkPolicyValidationOptions{AllowInvalidLabelValueInSelector: true}); len(errs) != 0 {
|
||||||
|
t.Errorf("Expected success, got %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
|
invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
|
||||||
@ -299,6 +318,9 @@ func TestValidateNetworkPolicy(t *testing.T) {
|
|||||||
"invalid ipv6 cidr format": makeNetworkPolicyCustom(setIngressFromIPBlockIPV6, func(networkPolicy *networking.NetworkPolicy) {
|
"invalid ipv6 cidr format": makeNetworkPolicyCustom(setIngressFromIPBlockIPV6, func(networkPolicy *networking.NetworkPolicy) {
|
||||||
networkPolicy.Spec.Ingress[0].From[0].IPBlock.CIDR = "fd00:192:168::"
|
networkPolicy.Spec.Ingress[0].From[0].IPBlock.CIDR = "fd00:192:168::"
|
||||||
}),
|
}),
|
||||||
|
"legacy cidr format with strict validation": makeNetworkPolicyCustom(setIngressFromIPBlockIPV4, func(networkPolicy *networking.NetworkPolicy) {
|
||||||
|
networkPolicy.Spec.Ingress[0].From[0].IPBlock.CIDR = "001.002.003.000/0"
|
||||||
|
}),
|
||||||
"except field is an empty string": makeNetworkPolicyCustom(setIngressFromIPBlockIPV4, func(networkPolicy *networking.NetworkPolicy) {
|
"except field is an empty string": makeNetworkPolicyCustom(setIngressFromIPBlockIPV4, func(networkPolicy *networking.NetworkPolicy) {
|
||||||
networkPolicy.Spec.Ingress[0].From[0].IPBlock.Except = []string{""}
|
networkPolicy.Spec.Ingress[0].From[0].IPBlock.Except = []string{""}
|
||||||
}),
|
}),
|
||||||
@ -367,9 +389,12 @@ func TestValidateNetworkPolicy(t *testing.T) {
|
|||||||
|
|
||||||
// Error cases are not expected to pass validation.
|
// Error cases are not expected to pass validation.
|
||||||
for testName, networkPolicy := range errorCases {
|
for testName, networkPolicy := range errorCases {
|
||||||
if errs := ValidateNetworkPolicy(networkPolicy, NetworkPolicyValidationOptions{AllowInvalidLabelValueInSelector: true}); len(errs) == 0 {
|
t.Run(testName, func(t *testing.T) {
|
||||||
t.Errorf("Expected failure for test: %s", testName)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, true)
|
||||||
}
|
if errs := ValidateNetworkPolicy(networkPolicy, NetworkPolicyValidationOptions{AllowInvalidLabelValueInSelector: true}); len(errs) == 0 {
|
||||||
|
t.Errorf("Expected failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,14 +442,98 @@ func TestValidateNetworkPolicyUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"fix pre-existing invalid CIDR": {
|
||||||
|
old: networking.NetworkPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
|
||||||
|
Spec: networking.NetworkPolicySpec{
|
||||||
|
PodSelector: metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
|
},
|
||||||
|
Ingress: []networking.NetworkPolicyIngressRule{{
|
||||||
|
From: []networking.NetworkPolicyPeer{{
|
||||||
|
IPBlock: &networking.IPBlock{
|
||||||
|
CIDR: "192.168.0.0/16",
|
||||||
|
Except: []string{
|
||||||
|
"192.168.2.0/24",
|
||||||
|
"192.168.3.1/24",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: networking.NetworkPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
|
||||||
|
Spec: networking.NetworkPolicySpec{
|
||||||
|
PodSelector: metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
|
},
|
||||||
|
Ingress: []networking.NetworkPolicyIngressRule{{
|
||||||
|
From: []networking.NetworkPolicyPeer{{
|
||||||
|
IPBlock: &networking.IPBlock{
|
||||||
|
CIDR: "192.168.0.0/16",
|
||||||
|
Except: []string{
|
||||||
|
"192.168.2.0/24",
|
||||||
|
"192.168.3.0/24",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"fail to fix pre-existing invalid CIDR": {
|
||||||
|
old: networking.NetworkPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
|
||||||
|
Spec: networking.NetworkPolicySpec{
|
||||||
|
PodSelector: metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
|
},
|
||||||
|
Ingress: []networking.NetworkPolicyIngressRule{{
|
||||||
|
From: []networking.NetworkPolicyPeer{{
|
||||||
|
IPBlock: &networking.IPBlock{
|
||||||
|
CIDR: "192.168.0.0/16",
|
||||||
|
Except: []string{
|
||||||
|
"192.168.2.0/24",
|
||||||
|
"192.168.3.1/24",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: networking.NetworkPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
|
||||||
|
Spec: networking.NetworkPolicySpec{
|
||||||
|
PodSelector: metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
|
},
|
||||||
|
Ingress: []networking.NetworkPolicyIngressRule{{
|
||||||
|
From: []networking.NetworkPolicyPeer{{
|
||||||
|
IPBlock: &networking.IPBlock{
|
||||||
|
CIDR: "192.168.0.0/16",
|
||||||
|
Except: []string{
|
||||||
|
"192.168.1.0/24",
|
||||||
|
"192.168.2.0/24",
|
||||||
|
"192.168.3.1/24",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for testName, successCase := range successCases {
|
for testName, successCase := range successCases {
|
||||||
successCase.old.ObjectMeta.ResourceVersion = "1"
|
t.Run(testName, func(t *testing.T) {
|
||||||
successCase.update.ObjectMeta.ResourceVersion = "1"
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, true)
|
||||||
if errs := ValidateNetworkPolicyUpdate(&successCase.update, &successCase.old, NetworkPolicyValidationOptions{false}); len(errs) != 0 {
|
successCase.old.ObjectMeta.ResourceVersion = "1"
|
||||||
t.Errorf("expected success (%s): %v", testName, errs)
|
successCase.update.ObjectMeta.ResourceVersion = "1"
|
||||||
}
|
if errs := ValidateNetworkPolicyUpdate(&successCase.update, &successCase.old, NetworkPolicyValidationOptions{}); len(errs) != 0 {
|
||||||
|
t.Errorf("expected success, but got %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
errorCases := map[string]npUpdateTest{
|
errorCases := map[string]npUpdateTest{
|
||||||
@ -444,14 +553,58 @@ func TestValidateNetworkPolicyUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"add new invalid CIDR": {
|
||||||
|
old: networking.NetworkPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
|
||||||
|
Spec: networking.NetworkPolicySpec{
|
||||||
|
PodSelector: metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
|
},
|
||||||
|
Ingress: []networking.NetworkPolicyIngressRule{{
|
||||||
|
From: []networking.NetworkPolicyPeer{{
|
||||||
|
IPBlock: &networking.IPBlock{
|
||||||
|
CIDR: "192.168.0.0/16",
|
||||||
|
Except: []string{
|
||||||
|
"192.168.2.0/24",
|
||||||
|
"192.168.3.0/24",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: networking.NetworkPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
|
||||||
|
Spec: networking.NetworkPolicySpec{
|
||||||
|
PodSelector: metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
|
},
|
||||||
|
Ingress: []networking.NetworkPolicyIngressRule{{
|
||||||
|
From: []networking.NetworkPolicyPeer{{
|
||||||
|
IPBlock: &networking.IPBlock{
|
||||||
|
CIDR: "192.168.0.0/16",
|
||||||
|
Except: []string{
|
||||||
|
"192.168.1.1/24",
|
||||||
|
"192.168.2.0/24",
|
||||||
|
"192.168.3.0/24",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for testName, errorCase := range errorCases {
|
for testName, errorCase := range errorCases {
|
||||||
errorCase.old.ObjectMeta.ResourceVersion = "1"
|
t.Run(testName, func(t *testing.T) {
|
||||||
errorCase.update.ObjectMeta.ResourceVersion = "1"
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, true)
|
||||||
if errs := ValidateNetworkPolicyUpdate(&errorCase.update, &errorCase.old, NetworkPolicyValidationOptions{false}); len(errs) == 0 {
|
errorCase.old.ObjectMeta.ResourceVersion = "1"
|
||||||
t.Errorf("expected failure: %s", testName)
|
errorCase.update.ObjectMeta.ResourceVersion = "1"
|
||||||
}
|
if errs := ValidateNetworkPolicyUpdate(&errorCase.update, &errorCase.old, NetworkPolicyValidationOptions{}); len(errs) == 0 {
|
||||||
|
t.Errorf("expected failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1805,6 +1958,14 @@ func TestValidateIngressStatusUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
legacyIP := newValid()
|
||||||
|
legacyIP.Status = networking.IngressStatus{
|
||||||
|
LoadBalancer: networking.IngressLoadBalancerStatus{
|
||||||
|
Ingress: []networking.IngressLoadBalancerIngress{
|
||||||
|
{IP: "001.002.003.004", Hostname: "foo.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
invalidHostname := newValid()
|
invalidHostname := newValid()
|
||||||
invalidHostname.Status = networking.IngressStatus{
|
invalidHostname.Status = networking.IngressStatus{
|
||||||
LoadBalancer: networking.IngressLoadBalancerStatus{
|
LoadBalancer: networking.IngressLoadBalancerStatus{
|
||||||
@ -1814,26 +1975,56 @@ func TestValidateIngressStatusUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := ValidateIngressStatusUpdate(&newValue, &oldValue)
|
successCases := map[string]struct {
|
||||||
if len(errs) != 0 {
|
oldValue networking.Ingress
|
||||||
t.Errorf("Unexpected error %v", errs)
|
newValue networking.Ingress
|
||||||
|
legacyIPs bool
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
oldValue: oldValue,
|
||||||
|
newValue: newValue,
|
||||||
|
},
|
||||||
|
"legacy IPs with legacy validation": {
|
||||||
|
oldValue: oldValue,
|
||||||
|
newValue: legacyIP,
|
||||||
|
legacyIPs: true,
|
||||||
|
},
|
||||||
|
"legacy IPs unchanged in update": {
|
||||||
|
oldValue: legacyIP,
|
||||||
|
newValue: legacyIP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for k, tc := range successCases {
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, !tc.legacyIPs)
|
||||||
|
|
||||||
|
errs := ValidateIngressStatusUpdate(&tc.newValue, &tc.oldValue)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("Unexpected error %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
errorCases := map[string]networking.Ingress{
|
errorCases := map[string]networking.Ingress{
|
||||||
"status.loadBalancer.ingress[0].ip: Invalid value": invalidIP,
|
"status.loadBalancer.ingress[0].ip: must be a valid IP address": invalidIP,
|
||||||
"status.loadBalancer.ingress[0].hostname: Invalid value": invalidHostname,
|
"status.loadBalancer.ingress[0].ip: must not have leading 0s": legacyIP,
|
||||||
|
"status.loadBalancer.ingress[0].hostname: must be a DNS name": invalidHostname,
|
||||||
}
|
}
|
||||||
for k, v := range errorCases {
|
for k, v := range errorCases {
|
||||||
errs := ValidateIngressStatusUpdate(&v, &oldValue)
|
t.Run(k, func(t *testing.T) {
|
||||||
if len(errs) == 0 {
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, true)
|
||||||
t.Errorf("expected failure for %s", k)
|
|
||||||
} else {
|
errs := ValidateIngressStatusUpdate(&v, &oldValue)
|
||||||
s := strings.Split(k, ":")
|
if len(errs) == 0 {
|
||||||
err := errs[0]
|
t.Errorf("expected failure")
|
||||||
if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) {
|
} else {
|
||||||
t.Errorf("unexpected error: %q, expected: %q", err, k)
|
s := strings.SplitN(k, ":", 2)
|
||||||
|
err := errs[0]
|
||||||
|
if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) {
|
||||||
|
t.Errorf("unexpected error: %q, expected: %q", err, k)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2150,13 +2341,13 @@ func TestValidateServiceCIDR(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"badip-iprange-caps-ipv6": {
|
"badip-iprange-caps-ipv6": {
|
||||||
expectedErrors: 2,
|
expectedErrors: 1,
|
||||||
ipRange: &networking.ServiceCIDR{
|
ipRange: &networking.ServiceCIDR{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "test-name",
|
Name: "test-name",
|
||||||
},
|
},
|
||||||
Spec: networking.ServiceCIDRSpec{
|
Spec: networking.ServiceCIDRSpec{
|
||||||
CIDRs: []string{"FD00:1234::2/64"},
|
CIDRs: []string{"FD00:1234::0/64"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -37,7 +37,6 @@ import (
|
|||||||
"k8s.io/dynamic-resource-allocation/structured"
|
"k8s.io/dynamic-resource-allocation/structured"
|
||||||
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
netutils "k8s.io/utils/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -890,25 +889,7 @@ func validateNetworkDeviceData(networkDeviceData *resource.NetworkDeviceData, fl
|
|||||||
|
|
||||||
allErrs = append(allErrs, validateSet(networkDeviceData.IPs, resource.NetworkDeviceDataMaxIPs,
|
allErrs = append(allErrs, validateSet(networkDeviceData.IPs, resource.NetworkDeviceDataMaxIPs,
|
||||||
func(address string, fldPath *field.Path) field.ErrorList {
|
func(address string, fldPath *field.Path) field.ErrorList {
|
||||||
// reformat CIDR to handle different ways IPs can be written
|
return validation.IsValidInterfaceAddress(fldPath, address)
|
||||||
// (e.g. 2001:db8::1/64 == 2001:0db8::1/64)
|
}, stringKey, fldPath.Child("ips"))...)
|
||||||
ip, ipNet, err := netutils.ParseCIDRSloppy(address)
|
|
||||||
if err != nil {
|
|
||||||
// must fail
|
|
||||||
return validation.IsValidCIDR(fldPath, address)
|
|
||||||
}
|
|
||||||
maskSize, _ := ipNet.Mask.Size()
|
|
||||||
canonical := fmt.Sprintf("%s/%d", ip.String(), maskSize)
|
|
||||||
if address != canonical {
|
|
||||||
return field.ErrorList{
|
|
||||||
field.Invalid(fldPath, address, fmt.Sprintf("must be in canonical form (%s)", canonical)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
func(address string) (string, string) {
|
|
||||||
return address, ""
|
|
||||||
},
|
|
||||||
fldPath.Child("ips"))...)
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
@ -1257,9 +1257,9 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
|
|||||||
wantFailures: field.ErrorList{
|
wantFailures: field.ErrorList{
|
||||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength),
|
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength),
|
||||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength),
|
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength),
|
||||||
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(0), "300.9.8.0/24", "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"),
|
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(0), "300.9.8.0/24", "must be a valid address in CIDR form, (e.g. 10.9.8.7/24 or 2001:db8::1/64)"),
|
||||||
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(1), "010.009.008.000/24", "must be in canonical form (10.9.8.0/24)"),
|
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(1), "010.009.008.000/24", "must be in canonical form (\"10.9.8.0/24\")"),
|
||||||
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(2), "2001:0db8::1/64", "must be in canonical form (2001:db8::1/64)"),
|
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(2), "2001:0db8::1/64", "must be in canonical form (\"2001:db8::1/64\")"),
|
||||||
},
|
},
|
||||||
oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(),
|
oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(),
|
||||||
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
@ -1390,7 +1390,7 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
|
|||||||
wantFailures: field.ErrorList{
|
wantFailures: field.ErrorList{
|
||||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength),
|
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength),
|
||||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength),
|
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength),
|
||||||
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(0), "300.9.8.0/24", "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"),
|
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(0), "300.9.8.0/24", "must be a valid address in CIDR form, (e.g. 10.9.8.7/24 or 2001:db8::1/64)"),
|
||||||
},
|
},
|
||||||
oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(),
|
oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(),
|
||||||
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
@ -715,6 +715,12 @@ const (
|
|||||||
// Allow API server Protobuf encoder to encode collections item by item, instead of all at once.
|
// Allow API server Protobuf encoder to encode collections item by item, instead of all at once.
|
||||||
StreamingCollectionEncodingToProtobuf featuregate.Feature = "StreamingCollectionEncodingToProtobuf"
|
StreamingCollectionEncodingToProtobuf featuregate.Feature = "StreamingCollectionEncodingToProtobuf"
|
||||||
|
|
||||||
|
// owner: @danwinship
|
||||||
|
// kep: https://kep.k8s.io/4858
|
||||||
|
//
|
||||||
|
// Requires stricter validation of IP addresses and CIDR values in API objects.
|
||||||
|
StrictIPCIDRValidation featuregate.Feature = "StrictIPCIDRValidation"
|
||||||
|
|
||||||
// owner: @robscott
|
// owner: @robscott
|
||||||
// kep: https://kep.k8s.io/2433
|
// kep: https://kep.k8s.io/2433
|
||||||
//
|
//
|
||||||
|
@ -766,6 +766,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
|
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
StrictIPCIDRValidation: {
|
||||||
|
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
|
||||||
|
},
|
||||||
|
|
||||||
TopologyAwareHints: {
|
TopologyAwareHints: {
|
||||||
{Version: version.MustParse("1.21"), Default: false, PreRelease: featuregate.Alpha},
|
{Version: version.MustParse("1.21"), Default: false, PreRelease: featuregate.Alpha},
|
||||||
{Version: version.MustParse("1.23"), Default: false, PreRelease: featuregate.Beta},
|
{Version: version.MustParse("1.23"), Default: false, PreRelease: featuregate.Beta},
|
||||||
|
@ -151,9 +151,7 @@ func (al *Allocators) initIPFamilyFields(after After, before Before) error {
|
|||||||
|
|
||||||
// Do some loose pre-validation of the input. This makes it easier in the
|
// Do some loose pre-validation of the input. This makes it easier in the
|
||||||
// rest of allocation code to not have to consider corner cases.
|
// rest of allocation code to not have to consider corner cases.
|
||||||
// TODO(thockin): when we tighten validation (e.g. to require IPs) we will
|
if el := validation.ValidateServiceClusterIPsRelatedFields(service, oldService); len(el) != 0 {
|
||||||
// need a "strict" and a "loose" form of this.
|
|
||||||
if el := validation.ValidateServiceClusterIPsRelatedFields(service); len(el) != 0 {
|
|
||||||
return errors.NewInvalid(api.Kind("Service"), service.Name, el)
|
return errors.NewInvalid(api.Kind("Service"), service.Name, el)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,28 +18,94 @@ package validation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
netutils "k8s.io/utils/net"
|
netutils "k8s.io/utils/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsValidIP tests that the argument is a valid IP address.
|
func parseIP(fldPath *field.Path, value string, strictValidation bool) (net.IP, field.ErrorList) {
|
||||||
func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
|
|
||||||
var allErrors field.ErrorList
|
var allErrors field.ErrorList
|
||||||
if netutils.ParseIPSloppy(value) == nil {
|
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)").WithOrigin("format=ip-sloppy"))
|
ip := netutils.ParseIPSloppy(value)
|
||||||
|
if ip == nil {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"))
|
||||||
|
return nil, allErrors
|
||||||
}
|
}
|
||||||
return allErrors
|
|
||||||
|
if strictValidation {
|
||||||
|
addr, err := netip.ParseAddr(value)
|
||||||
|
if err != nil {
|
||||||
|
// If netutils.ParseIPSloppy parsed it, but netip.ParseAddr
|
||||||
|
// doesn't, then it must have illegal leading 0s.
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have leading 0s"))
|
||||||
|
}
|
||||||
|
if addr.Is4In6() {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not be an IPv4-mapped IPv6 address"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip, allErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidIPForLegacyField tests that the argument is a valid IP address for a "legacy"
|
||||||
|
// API field that predates strict IP validation. In particular, this allows IPs that are
|
||||||
|
// not in canonical form (e.g., "FE80:0:0:0:0:0:0:0abc" instead of "fe80::abc").
|
||||||
|
//
|
||||||
|
// If strictValidation is false, this also allows IPs in certain invalid or ambiguous
|
||||||
|
// formats:
|
||||||
|
//
|
||||||
|
// 1. IPv4 IPs are allowed to have leading "0"s in octets (e.g. "010.002.003.004").
|
||||||
|
// Historically, net.ParseIP (and later netutils.ParseIPSloppy) simply ignored leading
|
||||||
|
// "0"s in IPv4 addresses, but most libc-based software treats 0-prefixed IPv4 octets
|
||||||
|
// as octal, meaning different software might interpret the same string as two
|
||||||
|
// different IPs, potentially leading to security issues. (Current net.ParseIP and
|
||||||
|
// netip.ParseAddr simply reject inputs with leading "0"s.)
|
||||||
|
//
|
||||||
|
// 2. IPv4-mapped IPv6 IPs (e.g. "::ffff:1.2.3.4") are allowed. These can also lead to
|
||||||
|
// different software interpreting the value in different ways, because they may be
|
||||||
|
// treated as IPv4 by some software and IPv6 by other software. (net.ParseIP and
|
||||||
|
// netip.ParseAddr both allow these, but there are no use cases for representing IPv4
|
||||||
|
// addresses as IPv4-mapped IPv6 addresses in Kubernetes.)
|
||||||
|
//
|
||||||
|
// Alternatively, when validating an update to an existing field, you can pass a list of
|
||||||
|
// IP values from the old object that should be accepted if they appear in the new object
|
||||||
|
// even if they are not valid.
|
||||||
|
//
|
||||||
|
// This function should only be used to validate the existing fields that were
|
||||||
|
// historically validated in this way, and strictValidation should be true unless the
|
||||||
|
// StrictIPCIDRValidation feature gate is disabled. Use IsValidIP for parsing new fields.
|
||||||
|
func IsValidIPForLegacyField(fldPath *field.Path, value string, strictValidation bool, validOldIPs []string) field.ErrorList {
|
||||||
|
if slices.Contains(validOldIPs, value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, allErrors := parseIP(fldPath, value, strictValidation)
|
||||||
|
return allErrors.WithOrigin("format=ip-sloppy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidIP tests that the argument is a valid IP address, according to current
|
||||||
|
// Kubernetes standards for IP address validation.
|
||||||
|
func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
|
||||||
|
ip, allErrors := parseIP(fldPath, value, true)
|
||||||
|
if len(allErrors) != 0 {
|
||||||
|
return allErrors.WithOrigin("format=ip-strict")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != ip.String() {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", ip.String())))
|
||||||
|
}
|
||||||
|
return allErrors.WithOrigin("format=ip-strict")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWarningsForIP returns warnings for IP address values in non-standard forms. This
|
// GetWarningsForIP returns warnings for IP address values in non-standard forms. This
|
||||||
// should only be used with fields that are validated with IsValidIP().
|
// should only be used with fields that are validated with IsValidIPForLegacyField().
|
||||||
func GetWarningsForIP(fldPath *field.Path, value string) []string {
|
func GetWarningsForIP(fldPath *field.Path, value string) []string {
|
||||||
ip := netutils.ParseIPSloppy(value)
|
ip := netutils.ParseIPSloppy(value)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
klog.ErrorS(nil, "GetWarningsForIP called on value that was not validated with IsValidIP", "field", fldPath, "value", value)
|
klog.ErrorS(nil, "GetWarningsForIP called on value that was not validated with IsValidIPForLegacyField", "field", fldPath, "value", value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,42 +130,88 @@ func GetWarningsForIP(fldPath *field.Path, value string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidIPv4Address tests that the argument is a valid IPv4 address.
|
func parseCIDR(fldPath *field.Path, value string, strictValidation bool) (*net.IPNet, field.ErrorList) {
|
||||||
func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList {
|
|
||||||
var allErrors field.ErrorList
|
var allErrors field.ErrorList
|
||||||
ip := netutils.ParseIPSloppy(value)
|
|
||||||
if ip == nil || ip.To4() == nil {
|
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address"))
|
|
||||||
}
|
|
||||||
return allErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValidIPv6Address tests that the argument is a valid IPv6 address.
|
_, ipnet, err := netutils.ParseCIDRSloppy(value)
|
||||||
func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList {
|
|
||||||
var allErrors field.ErrorList
|
|
||||||
ip := netutils.ParseIPSloppy(value)
|
|
||||||
if ip == nil || ip.To4() != nil {
|
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address"))
|
|
||||||
}
|
|
||||||
return allErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValidCIDR tests that the argument is a valid CIDR value.
|
|
||||||
func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList {
|
|
||||||
var allErrors field.ErrorList
|
|
||||||
_, _, err := netutils.ParseCIDRSloppy(value)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"))
|
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"))
|
||||||
|
return nil, allErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
if strictValidation {
|
||||||
|
prefix, err := netip.ParsePrefix(value)
|
||||||
|
if err != nil {
|
||||||
|
// If netutils.ParseCIDRSloppy parsed it, but netip.ParsePrefix
|
||||||
|
// doesn't, then it must have illegal leading 0s (either in the
|
||||||
|
// IP part or the prefix).
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have leading 0s in IP or prefix length"))
|
||||||
|
} else if prefix.Addr().Is4In6() {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have an IPv4-mapped IPv6 address"))
|
||||||
|
} else if prefix.Addr() != prefix.Masked().Addr() {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have bits set beyond the prefix length"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipnet, allErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidCIDRForLegacyField tests that the argument is a valid CIDR value for a "legacy"
|
||||||
|
// API field that predates strict IP validation. In particular, this allows IPs that are
|
||||||
|
// not in canonical form (e.g., "FE80:0abc:0:0:0:0:0:0/64" instead of "fe80:abc::/64").
|
||||||
|
//
|
||||||
|
// If strictValidation is false, this also allows CIDR values in certain invalid or
|
||||||
|
// ambiguous formats:
|
||||||
|
//
|
||||||
|
// 1. The IP part of the CIDR value is parsed as with IsValidIPForLegacyField with
|
||||||
|
// strictValidation=false.
|
||||||
|
//
|
||||||
|
// 2. The CIDR value is allowed to be either a "subnet"/"mask" (with the lower bits after
|
||||||
|
// the prefix length all being 0), or an "interface address" as with `ip addr` (with a
|
||||||
|
// complete IP address and associated subnet length). With strict validation, the
|
||||||
|
// value is required to be in "subnet"/"mask" form.
|
||||||
|
//
|
||||||
|
// 3. The prefix length is allowed to have leading 0s.
|
||||||
|
//
|
||||||
|
// Alternatively, when validating an update to an existing field, you can pass a list of
|
||||||
|
// CIDR values from the old object that should be accepted if they appear in the new
|
||||||
|
// object even if they are not valid.
|
||||||
|
//
|
||||||
|
// This function should only be used to validate the existing fields that were
|
||||||
|
// historically validated in this way, and strictValidation should be true unless the
|
||||||
|
// StrictIPCIDRValidation feature gate is disabled. Use IsValidCIDR or
|
||||||
|
// IsValidInterfaceAddress for parsing new fields.
|
||||||
|
func IsValidCIDRForLegacyField(fldPath *field.Path, value string, strictValidation bool, validOldCIDRs []string) field.ErrorList {
|
||||||
|
if slices.Contains(validOldCIDRs, value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, allErrors := parseCIDR(fldPath, value, strictValidation)
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidCIDR tests that the argument is a valid CIDR value, according to current
|
||||||
|
// Kubernetes standards for CIDR validation. This function is only for
|
||||||
|
// "subnet"/"mask"-style CIDR values (e.g., "192.168.1.0/24", with no bits set beyond the
|
||||||
|
// prefix length). Use IsValidInterfaceAddress for "ifaddr"-style CIDR values.
|
||||||
|
func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList {
|
||||||
|
ipnet, allErrors := parseCIDR(fldPath, value, true)
|
||||||
|
if len(allErrors) != 0 {
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != ipnet.String() {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", ipnet.String())))
|
||||||
}
|
}
|
||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWarningsForCIDR returns warnings for CIDR values in non-standard forms. This should
|
// GetWarningsForCIDR returns warnings for CIDR values in non-standard forms. This should
|
||||||
// only be used with fields that are validated with IsValidCIDR().
|
// only be used with fields that are validated with IsValidCIDRForLegacyField().
|
||||||
func GetWarningsForCIDR(fldPath *field.Path, value string) []string {
|
func GetWarningsForCIDR(fldPath *field.Path, value string) []string {
|
||||||
ip, ipnet, err := netutils.ParseCIDRSloppy(value)
|
ip, ipnet, err := netutils.ParseCIDRSloppy(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.ErrorS(err, "GetWarningsForCIDR called on value that was not validated with IsValidCIDR", "field", fldPath, "value", value)
|
klog.ErrorS(err, "GetWarningsForCIDR called on value that was not validated with IsValidCIDRForLegacyField", "field", fldPath, "value", value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,3 +249,30 @@ func GetWarningsForCIDR(fldPath *field.Path, value string) []string {
|
|||||||
|
|
||||||
return warnings
|
return warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValidInterfaceAddress tests that the argument is a valid "ifaddr"-style CIDR value in
|
||||||
|
// canonical form (e.g., "192.168.1.5/24", with a complete IP address and associated
|
||||||
|
// subnet length). Use IsValidCIDR for "subnet"/"mask"-style CIDR values (e.g.,
|
||||||
|
// "192.168.1.0/24").
|
||||||
|
func IsValidInterfaceAddress(fldPath *field.Path, value string) field.ErrorList {
|
||||||
|
var allErrors field.ErrorList
|
||||||
|
ip, ipnet, err := netutils.ParseCIDRSloppy(value)
|
||||||
|
if err != nil {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid address in CIDR form, (e.g. 10.9.8.7/24 or 2001:db8::1/64)"))
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// The canonical form of `value` is not `ipnet.String()`, because `ipnet` doesn't
|
||||||
|
// include the bits after the prefix. We need to construct the canonical form
|
||||||
|
// ourselves from `ip` and `ipnet.Mask`.
|
||||||
|
maskSize, _ := ipnet.Mask.Size()
|
||||||
|
if netutils.IsIPv4(ip) && maskSize > net.IPv4len*8 {
|
||||||
|
// "::ffff:192.168.0.1/120" -> "192.168.0.1/24"
|
||||||
|
maskSize -= (net.IPv6len - net.IPv4len) * 8
|
||||||
|
}
|
||||||
|
canonical := fmt.Sprintf("%s/%d", ip.String(), maskSize)
|
||||||
|
if value != canonical {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", canonical)))
|
||||||
|
}
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
@ -25,140 +25,193 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestIsValidIP(t *testing.T) {
|
func TestIsValidIP(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
in string
|
in string
|
||||||
family int
|
|
||||||
err string
|
err string
|
||||||
|
legacyErr string
|
||||||
|
legacyStrictErr string
|
||||||
}{
|
}{
|
||||||
// GOOD VALUES
|
// GOOD VALUES
|
||||||
{
|
{
|
||||||
name: "ipv4",
|
name: "ipv4",
|
||||||
in: "1.2.3.4",
|
in: "1.2.3.4",
|
||||||
family: 4,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4, all zeros",
|
name: "ipv4, all zeros",
|
||||||
in: "0.0.0.0",
|
in: "0.0.0.0",
|
||||||
family: 4,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4, max",
|
name: "ipv4, max",
|
||||||
in: "255.255.255.255",
|
in: "255.255.255.255",
|
||||||
family: 4,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6",
|
name: "ipv6",
|
||||||
in: "1234::abcd",
|
in: "1234::abcd",
|
||||||
family: 6,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6, all zeros, collapsed",
|
name: "ipv6, all zeros, collapsed",
|
||||||
in: "::",
|
in: "::",
|
||||||
family: 6,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6, max",
|
name: "ipv6, max",
|
||||||
in: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
|
in: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
|
||||||
family: 6,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// GOOD, THOUGH NON-CANONICAL, VALUES
|
// NON-CANONICAL VALUES
|
||||||
{
|
{
|
||||||
name: "ipv6, all zeros, expanded (non-canonical)",
|
name: "ipv6, all zeros, expanded (non-canonical)",
|
||||||
in: "0:0:0:0:0:0:0:0",
|
in: "0:0:0:0:0:0:0:0",
|
||||||
family: 6,
|
|
||||||
|
err: `must be in canonical form ("::")`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6, leading 0s (non-canonical)",
|
name: "ipv6, leading 0s (non-canonical)",
|
||||||
in: "0001:002:03:4::",
|
in: "0001:002:03:4::",
|
||||||
family: 6,
|
|
||||||
|
err: `must be in canonical form ("1:2:3:4::")`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6, capital letters (non-canonical)",
|
name: "ipv6, capital letters (non-canonical)",
|
||||||
in: "1234::ABCD",
|
in: "1234::ABCD",
|
||||||
family: 6,
|
|
||||||
|
err: `must be in canonical form ("1234::abcd")`,
|
||||||
},
|
},
|
||||||
|
|
||||||
// BAD VALUES WE CURRENTLY CONSIDER GOOD
|
// GOOD WITH LEGACY VALIDATION, BAD WITH STRICT VALIDATION
|
||||||
{
|
{
|
||||||
name: "ipv4 with leading 0s",
|
name: "ipv4 with leading 0s",
|
||||||
in: "1.1.1.01",
|
in: "1.1.1.01",
|
||||||
family: 4,
|
|
||||||
|
err: "must not have leading 0s",
|
||||||
|
legacyErr: "",
|
||||||
|
legacyStrictErr: "must not have leading 0s",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4-in-ipv6 value",
|
name: "ipv4-in-ipv6 value",
|
||||||
in: "::ffff:1.1.1.1",
|
in: "::ffff:1.1.1.1",
|
||||||
family: 4,
|
|
||||||
|
err: "must not be an IPv4-mapped IPv6 address",
|
||||||
|
legacyErr: "",
|
||||||
|
legacyStrictErr: "must not be an IPv4-mapped IPv6 address",
|
||||||
},
|
},
|
||||||
|
|
||||||
// BAD VALUES
|
// BAD VALUES
|
||||||
{
|
{
|
||||||
name: "empty string",
|
name: "empty string",
|
||||||
in: "",
|
in: "",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "junk",
|
name: "junk",
|
||||||
in: "aaaaaaa",
|
in: "aaaaaaa",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "domain name",
|
name: "domain name",
|
||||||
in: "myhost.mydomain",
|
in: "myhost.mydomain",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cidr",
|
name: "cidr",
|
||||||
in: "1.2.3.0/24",
|
in: "1.2.3.0/24",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4 with out-of-range octets",
|
name: "ipv4 with out-of-range octets",
|
||||||
in: "1.2.3.400",
|
in: "1.2.3.400",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4 with negative octets",
|
name: "ipv4 with negative octets",
|
||||||
in: "-1.0.0.0",
|
in: "-1.0.0.0",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6 with out-of-range segment",
|
name: "ipv6 with out-of-range segment",
|
||||||
in: "2001:db8::10005",
|
in: "2001:db8::10005",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4:port",
|
name: "ipv4:port",
|
||||||
in: "1.2.3.4:80",
|
in: "1.2.3.4:80",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6 with brackets",
|
name: "ipv6 with brackets",
|
||||||
in: "[2001:db8::1]",
|
in: "[2001:db8::1]",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "[ipv6]:port",
|
name: "[ipv6]:port",
|
||||||
in: "[2001:db8::1]:80",
|
in: "[2001:db8::1]:80",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "host:port",
|
name: "host:port",
|
||||||
in: "example.com:80",
|
in: "example.com:80",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6 with zone",
|
name: "ipv6 with zone",
|
||||||
in: "1234::abcd%eth0",
|
in: "1234::abcd%eth0",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4 with zone",
|
name: "ipv4 with zone",
|
||||||
in: "169.254.0.0%eth0",
|
in: "169.254.0.0%eth0",
|
||||||
err: "must be a valid IP address",
|
|
||||||
|
err: "must be a valid IP address",
|
||||||
|
legacyErr: "must be a valid IP address",
|
||||||
|
legacyStrictErr: "must be a valid IP address",
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
|
|
||||||
|
var badIPs []string
|
||||||
|
for _, tc := range testCases {
|
||||||
|
if tc.legacyStrictErr != "" {
|
||||||
|
badIPs = append(badIPs, tc.in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
errs := IsValidIP(field.NewPath(""), tc.in)
|
errs := IsValidIP(field.NewPath(""), tc.in)
|
||||||
if tc.err == "" {
|
if tc.err == "" {
|
||||||
@ -173,27 +226,36 @@ func TestIsValidIP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errs = IsValidIPv4Address(field.NewPath(""), tc.in)
|
errs = IsValidIPForLegacyField(field.NewPath(""), tc.in, false, nil)
|
||||||
if tc.family == 4 {
|
if tc.legacyErr == "" {
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
t.Errorf("expected %q to pass IsValidIPv4Address but got: %v", tc.in, errs)
|
t.Errorf("expected %q to be valid according to IsValidIPForLegacyField but got: %v", tc.in, errs)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if len(errs) == 0 {
|
if len(errs) != 1 {
|
||||||
t.Errorf("expected %q to fail IsValidIPv4Address", tc.in)
|
t.Errorf("expected %q to have 1 error from IsValidIPForLegacyField but got: %v", tc.in, errs)
|
||||||
|
} else if !strings.Contains(errs[0].Detail, tc.legacyErr) {
|
||||||
|
t.Errorf("expected error from IsValidIPForLegacyField for %q to contain %q but got: %q", tc.in, tc.legacyErr, errs[0].Detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errs = IsValidIPv6Address(field.NewPath(""), tc.in)
|
errs = IsValidIPForLegacyField(field.NewPath(""), tc.in, true, nil)
|
||||||
if tc.family == 6 {
|
if tc.legacyStrictErr == "" {
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
t.Errorf("expected %q to pass IsValidIPv6Address but got: %v", tc.in, errs)
|
t.Errorf("expected %q to be valid according to IsValidIPForLegacyField with strict validation, but got: %v", tc.in, errs)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if len(errs) == 0 {
|
if len(errs) != 1 {
|
||||||
t.Errorf("expected %q to fail IsValidIPv6Address", tc.in)
|
t.Errorf("expected %q to have 1 error from IsValidIPForLegacyField with strict validation, but got: %v", tc.in, errs)
|
||||||
|
} else if !strings.Contains(errs[0].Detail, tc.legacyStrictErr) {
|
||||||
|
t.Errorf("expected error from IsValidIPForLegacyField with strict validation for %q to contain %q but got: %q", tc.in, tc.legacyStrictErr, errs[0].Detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errs = IsValidIPForLegacyField(field.NewPath(""), tc.in, true, badIPs)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("expected %q to be accepted when using validOldIPs, but got: %v", tc.in, errs)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,10 +314,13 @@ func TestGetWarningsForIP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidCIDR(t *testing.T) {
|
func TestIsValidCIDR(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
in string
|
in string
|
||||||
err string
|
|
||||||
|
err string
|
||||||
|
legacyErr string
|
||||||
|
legacyStrictErr string
|
||||||
}{
|
}{
|
||||||
// GOOD VALUES
|
// GOOD VALUES
|
||||||
{
|
{
|
||||||
@ -283,79 +348,137 @@ func TestIsValidCIDR(t *testing.T) {
|
|||||||
in: "::1/128",
|
in: "::1/128",
|
||||||
},
|
},
|
||||||
|
|
||||||
// GOOD, THOUGH NON-CANONICAL, VALUES
|
// NON-CANONICAL VALUES
|
||||||
{
|
{
|
||||||
name: "ipv6, extra 0s (non-canonical)",
|
name: "ipv6, extra 0s (non-canonical)",
|
||||||
in: "2a00:79e0:2:0::/64",
|
in: "2a00:79e0:2:0::/64",
|
||||||
|
|
||||||
|
err: `must be in canonical form ("2a00:79e0:2::/64")`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6, capital letters (non-canonical)",
|
name: "ipv6, capital letters (non-canonical)",
|
||||||
in: "2001:DB8::/64",
|
in: "2001:DB8::/64",
|
||||||
|
|
||||||
|
err: `must be in canonical form ("2001:db8::/64")`,
|
||||||
},
|
},
|
||||||
|
|
||||||
// BAD VALUES WE CURRENTLY CONSIDER GOOD
|
// GOOD WITH LEGACY VALIDATION, BAD WITH STRICT VALIDATION
|
||||||
{
|
{
|
||||||
name: "ipv4 with leading 0s",
|
name: "ipv4 with leading 0s",
|
||||||
in: "1.1.01.0/24",
|
in: "1.1.01.0/24",
|
||||||
|
|
||||||
|
err: "must not have leading 0s in IP",
|
||||||
|
legacyErr: "",
|
||||||
|
legacyStrictErr: "must not have leading 0s in IP",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4-in-ipv6 with ipv4-sized prefix",
|
name: "ipv4-in-ipv6 with ipv4-sized prefix",
|
||||||
in: "::ffff:1.1.1.0/24",
|
in: "::ffff:1.1.1.0/24",
|
||||||
|
|
||||||
|
err: "must not have an IPv4-mapped IPv6 address",
|
||||||
|
legacyErr: "",
|
||||||
|
legacyStrictErr: "must not have an IPv4-mapped IPv6 address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4-in-ipv6 with ipv6-sized prefix",
|
name: "ipv4-in-ipv6 with ipv6-sized prefix",
|
||||||
in: "::ffff:1.1.1.0/120",
|
in: "::ffff:1.1.1.0/120",
|
||||||
|
|
||||||
|
err: "must not have an IPv4-mapped IPv6 address",
|
||||||
|
legacyErr: "",
|
||||||
|
legacyStrictErr: "must not have an IPv4-mapped IPv6 address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv4 with bits past prefix",
|
name: "ipv4 ifaddr",
|
||||||
in: "1.2.3.4/24",
|
in: "1.2.3.4/24",
|
||||||
|
|
||||||
|
err: "must not have bits set beyond the prefix length",
|
||||||
|
legacyErr: "",
|
||||||
|
legacyStrictErr: "must not have bits set beyond the prefix length",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ipv6 with bits past prefix",
|
name: "ipv6 ifaddr",
|
||||||
in: "2001:db8::1/64",
|
in: "2001:db8::1/64",
|
||||||
|
|
||||||
|
err: "must not have bits set beyond the prefix length",
|
||||||
|
legacyErr: "",
|
||||||
|
legacyStrictErr: "must not have bits set beyond the prefix length",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "prefix length with leading 0s",
|
name: "prefix length with leading 0s",
|
||||||
in: "192.168.0.0/016",
|
in: "192.168.0.0/016",
|
||||||
|
|
||||||
|
err: "must not have leading 0s",
|
||||||
|
legacyErr: "",
|
||||||
|
legacyStrictErr: "must not have leading 0s",
|
||||||
},
|
},
|
||||||
|
|
||||||
// BAD VALUES
|
// BAD VALUES
|
||||||
{
|
{
|
||||||
name: "empty string",
|
name: "empty string",
|
||||||
in: "",
|
in: "",
|
||||||
err: "must be a valid CIDR value",
|
|
||||||
|
err: "must be a valid CIDR value",
|
||||||
|
legacyErr: "must be a valid CIDR value",
|
||||||
|
legacyStrictErr: "must be a valid CIDR value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "junk",
|
name: "junk",
|
||||||
in: "aaaaaaa",
|
in: "aaaaaaa",
|
||||||
err: "must be a valid CIDR value",
|
|
||||||
|
err: "must be a valid CIDR value",
|
||||||
|
legacyErr: "must be a valid CIDR value",
|
||||||
|
legacyStrictErr: "must be a valid CIDR value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IP address",
|
name: "IP address",
|
||||||
in: "1.2.3.4",
|
in: "1.2.3.4",
|
||||||
err: "must be a valid CIDR value",
|
|
||||||
|
err: "must be a valid CIDR value",
|
||||||
|
legacyErr: "must be a valid CIDR value",
|
||||||
|
legacyStrictErr: "must be a valid CIDR value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "partial URL",
|
name: "partial URL",
|
||||||
in: "192.168.0.1/healthz",
|
in: "192.168.0.1/healthz",
|
||||||
err: "must be a valid CIDR value",
|
|
||||||
|
err: "must be a valid CIDR value",
|
||||||
|
legacyErr: "must be a valid CIDR value",
|
||||||
|
legacyStrictErr: "must be a valid CIDR value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "partial URL 2",
|
name: "partial URL 2",
|
||||||
in: "192.168.0.1/0/99",
|
in: "192.168.0.1/0/99",
|
||||||
err: "must be a valid CIDR value",
|
|
||||||
|
err: "must be a valid CIDR value",
|
||||||
|
legacyErr: "must be a valid CIDR value",
|
||||||
|
legacyStrictErr: "must be a valid CIDR value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "negative prefix length",
|
name: "negative prefix length",
|
||||||
in: "192.168.0.0/-16",
|
in: "192.168.0.0/-16",
|
||||||
err: "must be a valid CIDR value",
|
|
||||||
|
err: "must be a valid CIDR value",
|
||||||
|
legacyErr: "must be a valid CIDR value",
|
||||||
|
legacyStrictErr: "must be a valid CIDR value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "prefix length with sign",
|
name: "prefix length with sign",
|
||||||
in: "192.168.0.0/+16",
|
in: "192.168.0.0/+16",
|
||||||
err: "must be a valid CIDR value",
|
|
||||||
|
err: "must be a valid CIDR value",
|
||||||
|
legacyErr: "must be a valid CIDR value",
|
||||||
|
legacyStrictErr: "must be a valid CIDR value",
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
|
|
||||||
|
var badCIDRs []string
|
||||||
|
for _, tc := range testCases {
|
||||||
|
if tc.legacyStrictErr != "" {
|
||||||
|
badCIDRs = append(badCIDRs, tc.in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
errs := IsValidCIDR(field.NewPath(""), tc.in)
|
errs := IsValidCIDR(field.NewPath(""), tc.in)
|
||||||
if tc.err == "" {
|
if tc.err == "" {
|
||||||
@ -369,6 +492,37 @@ func TestIsValidCIDR(t *testing.T) {
|
|||||||
t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail)
|
t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errs = IsValidCIDRForLegacyField(field.NewPath(""), tc.in, false, nil)
|
||||||
|
if tc.legacyErr == "" {
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("expected %q to be valid according to IsValidCIDRForLegacyField but got: %v", tc.in, errs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(errs) != 1 {
|
||||||
|
t.Errorf("expected %q to have 1 error from IsValidCIDRForLegacyField but got: %v", tc.in, errs)
|
||||||
|
} else if !strings.Contains(errs[0].Detail, tc.legacyErr) {
|
||||||
|
t.Errorf("expected error for %q from IsValidCIDRForLegacyField to contain %q but got: %q", tc.in, tc.legacyErr, errs[0].Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errs = IsValidCIDRForLegacyField(field.NewPath(""), tc.in, true, nil)
|
||||||
|
if tc.legacyStrictErr == "" {
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("expected %q to be valid according to IsValidCIDRForLegacyField with strict validation but got: %v", tc.in, errs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(errs) != 1 {
|
||||||
|
t.Errorf("expected %q to have 1 error from IsValidCIDRForLegacyField with strict validation but got: %v", tc.in, errs)
|
||||||
|
} else if !strings.Contains(errs[0].Detail, tc.legacyStrictErr) {
|
||||||
|
t.Errorf("expected error for %q from IsValidCIDRForLegacyField with strict validation to contain %q but got: %q", tc.in, tc.legacyStrictErr, errs[0].Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errs = IsValidCIDRForLegacyField(field.NewPath(""), tc.in, true, badCIDRs)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("expected %q to be accepted when using validOldCIDRs, but got: %v", tc.in, errs)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -450,3 +604,106 @@ func TestGetWarningsForCIDR(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsValidInterfaceAddress(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
// GOOD VALUES
|
||||||
|
{
|
||||||
|
name: "ipv4",
|
||||||
|
in: "1.2.3.4/24",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4, single IP",
|
||||||
|
in: "1.1.1.1/32",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6",
|
||||||
|
in: "2001:4860:4860::1/48",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6, single IP",
|
||||||
|
in: "::1/128",
|
||||||
|
},
|
||||||
|
|
||||||
|
// BAD VALUES
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
in: "",
|
||||||
|
err: "must be a valid address in CIDR form",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "junk",
|
||||||
|
in: "aaaaaaa",
|
||||||
|
err: "must be a valid address in CIDR form",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IP address",
|
||||||
|
in: "1.2.3.4",
|
||||||
|
err: "must be a valid address in CIDR form",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partial URL",
|
||||||
|
in: "192.168.0.1/healthz",
|
||||||
|
err: "must be a valid address in CIDR form",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partial URL 2",
|
||||||
|
in: "192.168.0.1/0/99",
|
||||||
|
err: "must be a valid address in CIDR form",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative prefix length",
|
||||||
|
in: "192.168.0.0/-16",
|
||||||
|
err: "must be a valid address in CIDR form",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix length with sign",
|
||||||
|
in: "192.168.0.0/+16",
|
||||||
|
err: "must be a valid address in CIDR form",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6 non-canonical",
|
||||||
|
in: "2001:0:0:0::0BCD/64",
|
||||||
|
err: `must be in canonical form ("2001::bcd/64")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4 with leading 0s",
|
||||||
|
in: "1.1.01.002/24",
|
||||||
|
err: `must be in canonical form ("1.1.1.2/24")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4-in-ipv6 with ipv4-sized prefix",
|
||||||
|
in: "::ffff:1.1.1.1/24",
|
||||||
|
err: `must be in canonical form ("1.1.1.1/24")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv4-in-ipv6 with ipv6-sized prefix",
|
||||||
|
in: "::ffff:1.1.1.1/120",
|
||||||
|
err: `must be in canonical form ("1.1.1.1/24")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix length with leading 0s",
|
||||||
|
in: "192.168.0.5/016",
|
||||||
|
err: `must be in canonical form ("192.168.0.5/16")`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
errs := IsValidInterfaceAddress(field.NewPath(""), tc.in)
|
||||||
|
if tc.err == "" {
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("expected %q to be valid but got: %v", tc.in, errs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(errs) != 1 {
|
||||||
|
t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs)
|
||||||
|
} else if !strings.Contains(errs[0].Detail, tc.err) {
|
||||||
|
t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1388,6 +1388,12 @@
|
|||||||
lockToDefault: true
|
lockToDefault: true
|
||||||
preRelease: GA
|
preRelease: GA
|
||||||
version: "1.32"
|
version: "1.32"
|
||||||
|
- name: StrictIPCIDRValidation
|
||||||
|
versionedSpecs:
|
||||||
|
- default: false
|
||||||
|
lockToDefault: false
|
||||||
|
preRelease: Alpha
|
||||||
|
version: "1.33"
|
||||||
- name: StructuredAuthenticationConfiguration
|
- name: StructuredAuthenticationConfiguration
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: false
|
- default: false
|
||||||
|
Loading…
Reference in New Issue
Block a user