diff --git a/CHANGELOG.md b/CHANGELOG.md index be936ce1ccc..f0ca7594ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Fix #5954: (crd-generator) Sort required properties to ensure deterministic output * Fix #5973: CacheImpl locking for reading indexes (Cache.byIndex|indexKeys|index) was reduced * Fix #5953: Made informer watch starting deterministic with respect to list processing +* Fix #6012: Add convenience methods on HasMetadata to help with SSA #### Dependency Upgrade * Fix #5695: Upgrade Fabric8 Kubernetes Model to Kubernetes v1.30.0 diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java index 6fefacc5a8a..8c491c1158e 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java @@ -545,5 +545,4 @@ private static void schedule(Supplier> runner, long delay, }); }, delay, unit)); } - } diff --git a/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/HasMetadata.java b/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/HasMetadata.java index fd8f5e0e586..f82d637c482 100644 --- a/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/HasMetadata.java +++ b/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/HasMetadata.java @@ -48,10 +48,9 @@ public interface HasMetadata extends KubernetesResource { Pattern FINALIZER_NAME_MATCHER = Pattern.compile( "^((" + DNS_LABEL_REGEXP + "\\.)+" + DNS_LABEL_START + 2 + DNS_LABEL_END + ")/" + DNS_LABEL_REGEXP); - - ObjectMeta getMetadata(); - - void setMetadata(ObjectMeta metadata); + String REQUIRES_NON_NULL_METADATA = "requires non-null metadata"; + String REQUIRES_NON_NULL_NAME = "requires non-null name"; + String REQUIRES_NON_NULL_NAMESPACE = "requires non-null namespace"; /** * Retrieves the kind associated with the specified HasMetadata implementation. If the implementation is annotated with @@ -65,10 +64,6 @@ static String getKind(Class clazz) { return kind != null ? kind.value() : clazz.getSimpleName(); } - default String getKind() { - return getKind(getClass()); - } - /** * Computes the {@code apiVersion} associated with this HasMetadata implementation. The value is derived from the * {@link Group} and {@link Version} annotations. @@ -113,12 +108,6 @@ static String getVersion(Class clazz) { return version != null ? version.value() : null; } - default String getApiVersion() { - return getApiVersion(getClass()); - } - - void setApiVersion(String version); - /** * Retrieves the plural form associated with the specified class if annotated with {@link * Plural} or computes a default value using the value returned by {@link #getSingular(Class)} as @@ -133,11 +122,6 @@ static String getPlural(Class clazz) { : Pluralize.toPlural(getSingular(clazz))); } - @JsonIgnore - default String getPlural() { - return getPlural(getClass()); - } - /** * Retrieves the singular form associated with the specified class as defined by the * {@link Singular} annotation or computes a default value (lower-cased version of the value @@ -153,11 +137,6 @@ static String getSingular(Class clazz) { .toLowerCase(Locale.ROOT); } - @JsonIgnore - default String getSingular() { - return getSingular(getClass()); - } - static String getFullResourceName(Class clazz) { final String plural = getPlural(clazz); final String group = getGroup(clazz); @@ -175,6 +154,98 @@ static String getFullResourceName(String plural, String group) { return group.isEmpty() ? plural : plural + "." + group; } + /** + * Determines whether the specified finalizer is valid according to the + * finalizer + * specification. + * + * @param finalizer the identifier of the finalizer which validity we want to check + * @return {@code true} if the identifier is valid, {@code false} otherwise + */ + static boolean validateFinalizer(String finalizer) { + if (finalizer == null) { + return false; + } + final Matcher matcher = FINALIZER_NAME_MATCHER.matcher(finalizer); + if (matcher.matches()) { + final String group = matcher.group(1); + return group.length() < 256; + } else { + return false; + } + } + + /** + * Sanitizes and validates the specified {@link OwnerReference}, presumably to add it + * + * @param ownerReference the {@link OwnerReference} to sanitize and validate + * @return the sanitized and validated {@link OwnerReference} which should be used instead of the original one + */ + static OwnerReference sanitizeAndValidate(OwnerReference ownerReference) { + // validate required fields are present + final StringBuilder error = new StringBuilder(100); + error.append("Owner is missing required field(s): "); + final BiFunction> trimmedFieldIfValid = (field, value) -> { + boolean isError = false; + if (value == null) { + isError = true; + } else { + value = value.trim(); + if (value.isEmpty()) { + isError = true; + } + } + if (isError) { + error.append(field).append(" "); + return Optional.empty(); + } else { + return Optional.of(value); + } + }; + final Supplier exceptionSupplier = () -> new IllegalArgumentException( + error.toString()); + + final Optional uid = trimmedFieldIfValid.apply("uid", ownerReference.getUid()); + final Optional apiVersion = trimmedFieldIfValid.apply("apiVersion", + ownerReference.getApiVersion()); + final Optional name = trimmedFieldIfValid.apply("name", ownerReference.getName()); + final Optional kind = trimmedFieldIfValid.apply("kind", ownerReference.getKind()); + + // check that required values are present + ownerReference = new OwnerReferenceBuilder(ownerReference) + .withUid(uid.orElseThrow(exceptionSupplier)) + .withApiVersion(apiVersion.orElseThrow(exceptionSupplier)) + .withName(name.orElseThrow(exceptionSupplier)) + .withKind(kind.orElseThrow(exceptionSupplier)) + .build(); + return ownerReference; + } + + ObjectMeta getMetadata(); + + void setMetadata(ObjectMeta metadata); + + default String getKind() { + return getKind(getClass()); + } + + default String getApiVersion() { + return getApiVersion(getClass()); + } + + void setApiVersion(String version); + + @JsonIgnore + default String getPlural() { + return getPlural(getClass()); + } + + @JsonIgnore + default String getSingular() { + return getSingular(getClass()); + } + @JsonIgnore @SuppressWarnings("unused") default String getFullResourceName() { @@ -238,32 +309,9 @@ default boolean addFinalizer(String finalizer) { } /** - * Determines whether the specified finalizer is valid according to the - * finalizer - * specification. - * * @param finalizer the identifier of the finalizer which validity we want to check * @return {@code true} if the identifier is valid, {@code false} otherwise - */ - static boolean validateFinalizer(String finalizer) { - if (finalizer == null) { - return false; - } - final Matcher matcher = FINALIZER_NAME_MATCHER.matcher(finalizer); - if (matcher.matches()) { - final String group = matcher.group(1); - return group.length() < 256; - } else { - return false; - } - } - - /** * @see HasMetadata#validateFinalizer(String) - * - * @param finalizer the identifier of the finalizer which validity we want to check - * @return {@code true} if the identifier is valid, {@code false} otherwise */ default boolean isFinalizerValid(String finalizer) { return HasMetadata.validateFinalizer(finalizer); @@ -417,52 +465,6 @@ default OwnerReference addOwnerReference(OwnerReference ownerReference) { return ownerReference; } - /** - * Sanitizes and validates the specified {@link OwnerReference}, presumably to add it - * - * @param ownerReference the {@link OwnerReference} to sanitize and validate - * @return the sanitized and validated {@link OwnerReference} which should be used instead of the original one - */ - static OwnerReference sanitizeAndValidate(OwnerReference ownerReference) { - // validate required fields are present - final StringBuilder error = new StringBuilder(100); - error.append("Owner is missing required field(s): "); - final BiFunction> trimmedFieldIfValid = (field, value) -> { - boolean isError = false; - if (value == null) { - isError = true; - } else { - value = value.trim(); - if (value.isEmpty()) { - isError = true; - } - } - if (isError) { - error.append(field).append(" "); - return Optional.empty(); - } else { - return Optional.of(value); - } - }; - final Supplier exceptionSupplier = () -> new IllegalArgumentException( - error.toString()); - - final Optional uid = trimmedFieldIfValid.apply("uid", ownerReference.getUid()); - final Optional apiVersion = trimmedFieldIfValid.apply("apiVersion", - ownerReference.getApiVersion()); - final Optional name = trimmedFieldIfValid.apply("name", ownerReference.getName()); - final Optional kind = trimmedFieldIfValid.apply("kind", ownerReference.getKind()); - - // check that required values are present - ownerReference = new OwnerReferenceBuilder(ownerReference) - .withUid(uid.orElseThrow(exceptionSupplier)) - .withApiVersion(apiVersion.orElseThrow(exceptionSupplier)) - .withName(name.orElseThrow(exceptionSupplier)) - .withKind(kind.orElseThrow(exceptionSupplier)) - .build(); - return ownerReference; - } - /** * Removes the {@link OwnerReference} identified by the specified UID if it's part of this {@code HasMetadata}'s owner * references @@ -493,4 +495,40 @@ default void removeOwnerReference(HasMetadata owner) { default Optional optionalMetadata() { return Optional.ofNullable(getMetadata()); } + + /** + * Initializes this {@link ObjectMeta} field with name and namespace (if this instance represents a namespaced resource) + * provided by the specified HasMetadata instance. This is a convenience method to avoid boilerplate, notably when using + * Server-Side Apply, when creating a new instance with only some fields of the original one. Calls + * {@link #setMetadata(ObjectMeta)} when done, if you want to further configure this instance's metadata, please use + * {@link #initMetadataBuilderNameAndNamespaceFrom(HasMetadata)} instead, which doesn't sets the metadata, leaving it + * up to the user once configuration is finished. + * + * @param original a HasMetadata instance from which to retrieve the name and namespace + */ + default void initNameAndNamespaceFrom(HasMetadata original) { + Objects.requireNonNull(original); + final ObjectMeta meta = initMetadataBuilderNameAndNamespaceFrom(original).build(); + setMetadata(meta); + } + + /** + * Creates and initializes a new {@link ObjectMetaBuilder} with name and namespace (if the provided instance to initialize + * from represents a namespaced resource) provided by the specified HasMetadata instance. This is a convenience method to + * avoid boilerplate, notably when using Server-Side Apply, when creating a new instance with only some fields of the original + * one. This method assumes that further configuration will occur on the newly created ObjectMetaBuilder. + * + * @param original an HasMetadata instance from which to retrieve the name and namespace + * @return a new ObjectMetaBuilder instance initialized with the name and namespace (if needed) of the specified HasMetadata + */ + static ObjectMetaBuilder initMetadataBuilderNameAndNamespaceFrom(HasMetadata original) { + Objects.requireNonNull(original); + final ObjectMeta metadata = Objects.requireNonNull(original.getMetadata(), REQUIRES_NON_NULL_METADATA); + final ObjectMetaBuilder metaBuilder = new ObjectMetaBuilder(); + metaBuilder.withName(Objects.requireNonNull(metadata.getName(), REQUIRES_NON_NULL_NAME)); + if (original instanceof Namespaced) { + metaBuilder.withNamespace(Objects.requireNonNull(metadata.getNamespace(), REQUIRES_NON_NULL_NAMESPACE)); + } + return metaBuilder; + } } diff --git a/kubernetes-model-generator/kubernetes-model-core/src/test/java/io/fabric8/kubernetes/api/model/HasMetadataTest.java b/kubernetes-model-generator/kubernetes-model-core/src/test/java/io/fabric8/kubernetes/api/model/HasMetadataTest.java index ec007b22a3e..a1a76219537 100644 --- a/kubernetes-model-generator/kubernetes-model-core/src/test/java/io/fabric8/kubernetes/api/model/HasMetadataTest.java +++ b/kubernetes-model-generator/kubernetes-model-core/src/test/java/io/fabric8/kubernetes/api/model/HasMetadataTest.java @@ -235,6 +235,104 @@ void addingOwnerReferenceToResourceInDifferentNamespaceShouldFail() { assertThrows(IllegalArgumentException.class, () -> namespaced1.addOwnerReference(namespaced2)); } + @Test + void testInitNameAndNamespaceFromNamespacedResource() { + TestNamespacedHasMetadata original = new TestNamespacedHasMetadata(); + ObjectMeta originalMetadata = new ObjectMetaBuilder() + .withName("testName") + .withNamespace("testNamespace") + .withGeneration(100000L) + .build(); + original.setMetadata(originalMetadata); + + TestNamespacedHasMetadata copy = new TestNamespacedHasMetadata(); + copy.initNameAndNamespaceFrom(original); + + final ObjectMeta metadata = copy.getMetadata(); + assertEquals(originalMetadata.getName(), metadata.getName()); + assertEquals(originalMetadata.getNamespace(), metadata.getNamespace()); + assertNull(metadata.getGeneration()); + } + + @Test + void testInitNameAndNamespaceFromClusteredResource() { + TestHasMetadata original = new TestHasMetadata(); + ObjectMeta originalMetadata = new ObjectMetaBuilder() + .withName("testName") + .withGeneration(100000L) + .build(); + original.setMetadata(originalMetadata); + + TestHasMetadata copy = new TestHasMetadata(); + copy.initNameAndNamespaceFrom(original); + + final ObjectMeta metadata = copy.getMetadata(); + assertEquals(originalMetadata.getName(), metadata.getName()); + assertNull(metadata.getNamespace()); + assertNull(metadata.getGeneration()); + } + + @Test + void initNameAndNamespaceFromWithNullMetadataShouldFail() { + TestHasMetadata original = new TestHasMetadata(); + original.setMetadata(null); + + final TestHasMetadata test = new TestHasMetadata(); + Exception exception = assertThrows(NullPointerException.class, + () -> test.initNameAndNamespaceFrom(original)); + assertEquals(HasMetadata.REQUIRES_NON_NULL_METADATA, exception.getMessage()); + } + + @Test + void initNameAndNamespaceFromWithMissingNameShouldFail() { + TestNamespacedHasMetadata original = new TestNamespacedHasMetadata(); + ObjectMeta originalMetadata = new ObjectMetaBuilder() + .withNamespace("testNamespace") + .build(); + original.setMetadata(originalMetadata); + + final TestNamespacedHasMetadata test = new TestNamespacedHasMetadata(); + Exception exception = assertThrows(NullPointerException.class, + () -> test.initNameAndNamespaceFrom(original)); + assertEquals(HasMetadata.REQUIRES_NON_NULL_NAME, exception.getMessage()); + } + + @Test + void initNameAndNamespaceFromWithMissingNamespaceShouldFail() { + TestNamespacedHasMetadata original = new TestNamespacedHasMetadata(); + ObjectMeta originalMetadata = new ObjectMetaBuilder() + .withName("testName") + .build(); + original.setMetadata(originalMetadata); + + final TestNamespacedHasMetadata test = new TestNamespacedHasMetadata(); + Exception exception = assertThrows(NullPointerException.class, + () -> test.initNameAndNamespaceFrom(original)); + assertEquals(HasMetadata.REQUIRES_NON_NULL_NAMESPACE, exception.getMessage()); + } + + static class TestHasMetadata implements HasMetadata { + private ObjectMeta metadata; + + @Override + public ObjectMeta getMetadata() { + return metadata; + } + + @Override + public void setMetadata(ObjectMeta metadata) { + this.metadata = metadata; + } + + @Override + public void setApiVersion(String version) { + } + } + + static class TestNamespacedHasMetadata extends TestHasMetadata implements Namespaced { + // No additional fields or methods needed for this simple test implementation + } + @Group("fabric8.io") @Version("v1") private static class Woman implements HasMetadata {