Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,79 @@ static ObjectMetaBuilder initMetadataBuilderNameAndNamespaceFrom(HasMetadata ori
}
return metaBuilder;
}

/**
* Same as {@code isSameResource(other, false)}
*
* @see #isSameResource(HasMetadata, boolean)
*/
default boolean isSameResource(HasMetadata other) {
return isSameResource(other, false);
}

/**
* Whether the specified HasMetadata represents the same resource on the cluster as this one, i.e. kind, uid, name and
* namespace all point to the same logical resource.
*
* <p>
* Note that this doesn't make any other assumption regarding other fields, and in particular,
* {@code resource1.isSameResource(resource2)} does NOT imply {@code resource1.equals(resource2)}.
* </p>
*
* @param other the HasMetadata to check
* @param strict whether the checks should be strict:
* <ul>
* <li>make sure that {@code kind} is properly set for both resources and that they are equal</li>
* <li>check that the {@code resourceVersion} is the same for both, if set or {@code null} for both</li>
* </ul>
* The only exceptions to this is if the two resources point to the same object (i.e. {@code this == other}) or if the
* UID is set, in which case stricter checks will not be enforced and similarity will only be assessed on the objects'
* identity (either Java identity or via UID).
* @return {@code true} if the specified HasMetadata points to the same cluster resource, {@code false} otherwise
*/
default boolean isSameResource(HasMetadata other, boolean strict) {
if (this == other) {
return true;
}

if (other == null) {
return false;
}

final var metadata = this.getMetadata();
final var otherMetadata = other.getMetadata();
if (metadata == null || otherMetadata == null) {
return false;
}

// check UID first, if either is not null, if both are null, look at other fields
final var uid = metadata.getUid();
final var otherUID = otherMetadata.getUid();
if (uid != null || otherUID != null) {
return Objects.equals(uid, otherUID);
}

final var kind = getKind();
Copy link
Copy Markdown
Member

@manusa manusa Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should apiVersion be checked too? (e.g. two resources with same kind but different apiVersion v1beta1 vs. v1alpha1 (or a real legacy case such as apps/v1 vs extensions/v1beta1 Deployment) would be considered the same)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first implementation was doing that and then I read in the Kube docs that an object's identity shouldn't take the API version into consideration… cf https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names

We could have an option to also check the API version, if we think it's interesting to do so.

Another question I had, was with respect to the UID… I'm not sure about how it's used and whether or not it should factor in this function.

Copy link
Copy Markdown
Contributor

@csviri csviri Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was just going to ask that, if UID is present on both resources on that case probably would be enough to check that.

Copy link
Copy Markdown
Contributor

@csviri csviri Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API version is tricky, since there should be a conversion hook, and there is always just one stored version, not sure if after the conversion it preserves UID, would be worth to check.
Is is a matter of definition too, what we call "same resource".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be a little cautious introducing notions like this which are not generic kubernetes terms

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question I had wrt to UID is that I was wondering if it's possible for a logical resource which would be recreated with the same UID (since it's supposed to be the same resource) could end up with a different name and namespace? I don't really have much experience dealing with UIDs so I'm not too sure if that's a valid use case or not…

Copy link
Copy Markdown
Contributor

@csviri csviri Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question I had wrt to UID is that I was wondering if it's possible for a logical resource which would be recreated with the same UID (since it's supposed to be the same resource) could end up with a different name and namespace?

Here we are comparing two resources where both should live on the server (or at least live close in time), theoretically this could happen, but I don't think we should care. The probability of UID collision is already:

For example, the number of random version 4 UUIDs which need to be generated in order to have a 50% probability of at least one collision is 2.71 quintillion.
And we are talking about a short period of time this to happen.

If that happens I will eat my red hat :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you manage to check the UID preservation after the conversion hook is applied?
From my side I'm happy to merge this since you folks (and downstream users of JOSDK) are probably the ones who will be consuming it.

final var differentKind = !Objects.equals(other.getKind(), kind);
if (differentKind) {
return false;
}

// in strict mode, the kind needs to be set
if (strict && kind == null) {
return false;
}

final var sameNameAndNS = Objects.equals(metadata.getName(), otherMetadata.getName())
&& Objects.equals(metadata.getNamespace(), otherMetadata.getNamespace());
if (!sameNameAndNS) {
return false;
}

if (strict) {
return Objects.equals(metadata.getResourceVersion(), otherMetadata.getResourceVersion());
} else {
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import io.fabric8.kubernetes.model.annotation.Version;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.Random;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
Expand Down Expand Up @@ -316,6 +318,80 @@ void initNameAndNamespaceFromWithMissingNamespaceShouldFail() {
assertEquals(HasMetadata.REQUIRES_NON_NULL_NAMESPACE, exception.getMessage());
}

@Test
void isSameResource() {
HasMetadata one = new TestHasMetadata();
assertFalse(one.isSameResource(null));

HasMetadata two = new TestHasMetadata();
assertTrue(one.isSameResource(one));
assertTrue(one.isSameResource(one, true)); // should normally be false but since we're comparing the same object, it should always be true

assertFalse(one.isSameResource(two)); // metadata is null so cannot check if resources are the same
assertFalse(one.isSameResource(two, true));

// generated name and namespace
TestHM t1 = new TestHM();
TestHM t2 = new TestHM();
checkDifferent(t1, t2);

// if UID is set, nothing else is checked, regardless of strict mode or not
t1 = new TestHM();
t2 = new TestHM(t1);
final var uid = randomString();
t1.getMetadata().setUid(uid);
checkDifferent(t1, t2);
t2.getMetadata().setUid(uid);
baseTests(t1, t2);
assertTrue(t1.isSameResource(t2, true));
assertTrue(t2.isSameResource(t1, true));

t1 = new TestHM();
t2 = new TestHM(t1); // initialized with same name and namespace
baseTests(t1, t2);
assertTrue(t1.isSameResource(t2, true));
assertTrue(t2.isSameResource(t1, true));

t1.getMetadata().setResourceVersion("t1");
t2.getMetadata().setResourceVersion("t1");
baseTests(t1, t2);
assertTrue(t1.isSameResource(t2, true));
assertTrue(t2.isSameResource(t1, true));

t2.getMetadata().setResourceVersion("t2");
baseTests(t1, t2);
assertFalse(t1.isSameResource(t2, true));
assertFalse(t2.isSameResource(t1, true));

t2.getMetadata().setName("otherName");
checkDifferent(t1, t2);

t1 = new TestHM();
t2 = new TestHM(t1); // initialized with same name and namespace
t2.getMetadata().setNamespace("otherNamespace");
checkDifferent(t1, t2);
}

private static String randomString() {
byte[] array = new byte[10];
new Random().nextBytes(array);
return new String(array, StandardCharsets.UTF_8);
}

private static void checkDifferent(TestHM t1, TestHM t2) {
assertFalse(t1.isSameResource(t2));
assertFalse(t2.isSameResource(t1));
assertFalse(t1.isSameResource(t2, true));
assertFalse(t2.isSameResource(t1, true));
}

private static void baseTests(HasMetadata t1, HasMetadata t2) {
assertTrue(t1.isSameResource(t1));
assertTrue(t1.isSameResource(t1, true));
assertTrue(t1.isSameResource(t2));
assertTrue(t2.isSameResource(t1));
}

static class TestHasMetadata implements HasMetadata {
private ObjectMeta metadata;

Expand Down Expand Up @@ -442,6 +518,11 @@ private static class Default implements HasMetadata {
meta.setName(name);
}

Default(String name, String namespace) {
this(name);
meta.setNamespace(namespace);
}

@Override
public ObjectMeta getMetadata() {
return meta;
Expand All @@ -468,6 +549,23 @@ public void setApiVersion(String version) {
}
}

@Group("fabric8.io")
@Version("v1")
private static class TestHM extends Default {
public TestHM(TestHM other) {
super(other.getMetadata().getName(), other.getMetadata().getNamespace());
}

public TestHM() {
super(randomString(), randomString());
}

@Override
public String getKind() {
return "TestHM";
}
}

private static class OwnerNamespaced extends Owner implements Namespaced {
}

Expand Down
Loading