diff --git a/CHANGELOG.md b/CHANGELOG.md index 4051ebc4271..8ae156e9984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * Fix #5377: added a createOr and unlock function to provide a straight-forward replacement for createOrReplace. * Fix #5388: [crd-generator] Generate deterministic CRDs * Fix #5257: Add ErrorStreamMessage and StatusStreamMessage to ease mocking of pods/exec requests +* Fix #5220: refinements and clarifications to the validation of names #### Dependency Upgrade * Fix #5373: Gradle base API based on v8.2.1 @@ -22,6 +23,7 @@ * Fix #5343: Removed `io.fabric8.kubernetes.model.annotation.PrinterColumn`, use `io.fabric8.crd.generator.annotation.PrinterColumn` * Fix #5368: ListOptions parameter ordering is now alphabetical. If you are using non-crud mocking for lists with options, you may need to update your parameter order. * Fix #5391: Removed the vertx-uri-template dependency from the vertx client, if you need that for your application, then introduce your own dependency. +* Fix #5220: KubernetesResourceUtil.isValidLabelOrAnnotation has been deprecated because the rules for labels and annotations are different ### 6.8.1 (2023-08-14) diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java index ff99f69e28a..1dc5e54c4ab 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java @@ -60,7 +60,12 @@ public class KubernetesResourceUtil { private KubernetesResourceUtil() { } + public static final Pattern KUBERNETES_SUBDOMAIN_REGEX = Pattern + .compile("[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"); public static final Pattern KUBERNETES_DNS1123_LABEL_REGEX = Pattern.compile("[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?"); + public static final Pattern KUBERNETES_KEY_REGEX = Pattern + .compile("(" + KUBERNETES_SUBDOMAIN_REGEX.toString() + "/)?[a-z0-9]([-_.a-z0-9]{0,61}[a-z0-9])?"); + private static final Pattern INVALID_LABEL_CHARS_PATTERN = Pattern.compile("[^-A-Za-z0-9]+"); private static final String DEFAULT_CONTAINER_IMAGE_REGISTRY_SECRET_NAME = "container-image-registry-secret"; @@ -249,7 +254,11 @@ public static Map getOrCreateAnnotations(HasMetadata entity) { } /** - * Returns an identifier from the given string that can be used as resource name. + * Returns an identifier from the given string that can be used as resource, label key/value, or annotation key + * in accordance to RFC 1123 Label Names. + *

+ * Note that this is more restrictive than necessary for most resources and label/annotation keys. It will truncate the name + * if necessary, which may affect uniqueness. * * @param name which needs to be sanitized * @return sanitized name @@ -324,7 +333,15 @@ public static ObjectMeta getOrCreateMetadata(HasMetadata entity) { } /** - * Validates name of Kubernetes Resource name, label or annotation based on Kubernetes regex + * Validates name of a Kubernetes Resource name, label key/value or annotation key based on RFC 1123 Label Names. + *

+ * Note: this is more restrictive than what is allowed for annotation names or most resource names, however some resource + * types have additional restrictions on their names. + *

+ * Refer to Kubernetes Naming + * Conventions + *

+ * See also {@link #isValidKey(String)} and {@link #isValidSubdomainName(String)} * * @param name Name of resource/label/annotation * @return returns a boolean value indicating whether it's valid or not @@ -333,19 +350,62 @@ public static boolean isValidName(String name) { return Utils.isNotNullOrEmpty(name) && KUBERNETES_DNS1123_LABEL_REGEX.matcher(name).matches(); } + /** + * Validates annotation or label key. + * + * @param key the annotation or label key + * @return returns a boolean value indicating whether it's valid or not + */ + public static boolean isValidKey(String key) { + return Utils.isNotNullOrEmpty(key) && key.length() < 254 && KUBERNETES_KEY_REGEX.matcher(key).matches(); + } + + /** + * Validates name of an resource according to DNS Subdomain rules. + *

+ * Refer to Kubernetes Naming + * Conventions + * + * @param name Name of annotation + * @return returns a boolean value indicating whether it's valid or not + */ + public static boolean isValidSubdomainName(String name) { + return Utils.isNotNullOrEmpty(name) && name.length() < 254 && KUBERNETES_SUBDOMAIN_REGEX.matcher(name).matches(); + } + /** * Validates labels/annotations of Kubernetes resources * * @param map Label/Annotation of resource * @return returns a boolean value indicating whether it's valid or not + * + * @see #areLabelsValid(Map) {@link #areAnnotationsValid(Map)} + * + * @deprecated the rules are different for label and annotation values */ + @Deprecated public static boolean isValidLabelOrAnnotation(Map map) { - for (Map.Entry entry : map.entrySet()) { - if (!(isValidName(entry.getKey()) && isValidName(entry.getValue()))) { - return false; - } - } - return true; + return areLabelsValid(map); + } + + /** + * Checks the given map keys and values for validity as labels. + * + * @param map labels + * @return returns a boolean value indicating whether it's valid or not + */ + public static boolean areLabelsValid(Map map) { + return map.entrySet().stream().allMatch(e -> isValidKey(e.getKey()) && isValidName(e.getValue())); + } + + /** + * Checks the given map keys validity as annotations. + * + * @param map annotations + * @return returns a boolean value indicating whether it's valid or not + */ + public static boolean areAnnotationsValid(Map map) { + return map.keySet().stream().allMatch(KubernetesResourceUtil::isValidKey); } /** diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtilTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtilTest.java index 0199aa7be1c..e2ced0ed10a 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtilTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtilTest.java @@ -122,9 +122,9 @@ void testNullSafeOperationsForAnnotations() { void testNames() { assertTrue(KubernetesResourceUtil.isValidName(KubernetesResourceUtil.getName(configMap1))); assertFalse(KubernetesResourceUtil.isValidName("test.invalid.name")); - assertTrue(KubernetesResourceUtil.isValidLabelOrAnnotation(KubernetesResourceUtil.getOrCreateAnnotations(configMap1))); + assertTrue(KubernetesResourceUtil.areLabelsValid(KubernetesResourceUtil.getOrCreateAnnotations(configMap1))); assertFalse(KubernetesResourceUtil - .isValidLabelOrAnnotation(Collections.singletonMap("NoUppercaseOrSpecialCharsLike=Equals", "bar"))); + .areLabelsValid(Collections.singletonMap("NoUppercaseOrSpecialCharsLike=Equals", "bar"))); assertTrue(KubernetesResourceUtil.isValidName(KubernetesResourceUtil.sanitizeName("test.invalid.name"))); assertTrue(KubernetesResourceUtil.isValidName(KubernetesResourceUtil.sanitizeName("90notcool-n@me"))); @@ -132,6 +132,24 @@ void testNames() { KubernetesResourceUtil.sanitizeName("90notcool-n@me_______waytoooooooooolooooooooongand should be shorten for sure"))); } + @Test + void testSubdomainValidation() { + assertTrue(KubernetesResourceUtil.isValidSubdomainName("a.b")); + assertFalse(KubernetesResourceUtil.isValidSubdomainName("a..b")); + } + + @Test + void testKeyValidation() { + assertTrue(KubernetesResourceUtil.isValidKey("domain.prefix.io/label.key")); + assertFalse(KubernetesResourceUtil.isValidKey("domain.prefix.io?label.key")); + } + + @Test + void testAnnotationValidation() { + assertTrue( + KubernetesResourceUtil.areAnnotationsValid(Collections.singletonMap("simple-key", "Something not valid for a label!"))); + } + @Test void testSortEventListBasedOnTimestamp() { // Given