-
Notifications
You must be signed in to change notification settings - Fork 235
[WIP] improve: owner reference mappers checks only group not apiVersion #3302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,8 @@ | |
| import io.javaoperatorsdk.operator.processing.event.ResourceID; | ||
| import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; | ||
|
|
||
| import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getGroup; | ||
|
|
||
| public class Mappers { | ||
|
|
||
| public static final String DEFAULT_ANNOTATION_FOR_NAME = "io.javaoperatorsdk/primary-name"; | ||
|
|
@@ -98,10 +100,12 @@ public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromOwnerRefer | |
|
|
||
| public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromOwnerReferences( | ||
| String apiVersion, String kind, boolean clusterScope) { | ||
| String correctApiVersion = apiVersion.startsWith("/") ? apiVersion.substring(1) : apiVersion; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @metacosm see this part. Also if you check the related commit, that explains the issue here.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, that's weird, I wonder why we have that code here to start with… Let me look into it.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, this was a workaround for a bug in the client which has been addressed in fabric8io/kubernetes-client@84f46da so we should actually remove this workaround as well now.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great, thank you for checking! Will update the PR. |
||
| return resource -> | ||
| resource.getMetadata().getOwnerReferences().stream() | ||
| .filter(r -> r.getKind().equals(kind) && r.getApiVersion().equals(correctApiVersion)) | ||
| .filter( | ||
| r -> | ||
| r.getKind().equals(kind) | ||
| && getGroup(r.getApiVersion()).equals(getGroup(apiVersion))) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is the correct behavior: shouldn't there be a conversion hook instead? It seems dangerous to potentially match resources with differing versions…
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Conversion hooks, does not update the owner references of the resource. Maybe we should have an integration test also this, to see how exactly and what happens. I will add one. |
||
| .map(or -> ResourceID.fromOwnerReference(resource, or, clusterScope)) | ||
| .collect(Collectors.toSet()); | ||
|
Comment on lines
101
to
110
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,7 @@ | |
| import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultFinalizerName; | ||
| import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultNameFor; | ||
| import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultReconcilerName; | ||
| import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getGroup; | ||
| import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.handleKubernetesClientException; | ||
| import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.isFinalizerValid; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
@@ -308,6 +309,14 @@ void compareResourceVersionsWithHasMetadata() { | |
| assertThat(ReconcilerUtilsInternal.compareResourceVersions(resource2, resource1)).isPositive(); | ||
| } | ||
|
|
||
| @Test | ||
| void extractsGroupFromApiVersion() { | ||
| assertThat(getGroup("v1")).isEqualTo(""); | ||
| assertThat(getGroup("/v1")).isEqualTo(""); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't a valid apiVersion String
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see conversation here: #3302 (comment) |
||
| assertThat(getGroup("apps/v1")).isEqualTo("apps"); | ||
| assertThat(getGroup("/apps/v1")).isEqualTo("apps"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't a valid apiVersion so I don't think we should cover this case, apart maybe to throw an IllegalArgumentException
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made this so we are backwards compatible with the current implementation marked you on the related code part. But I tend to agree that we should make it throw such exception in next minor version.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the client does not have the underlying issue anymore, I don't think that we should implement these backwards compatible checks since they shouldn't be needed anymore. |
||
| } | ||
|
|
||
| private HasMetadata createResourceWithVersion(String resourceVersion) { | ||
| return new PodBuilder() | ||
| .withMetadata( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /* | ||
| * Copyright Java Operator SDK Authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion; | ||
|
|
||
| import io.fabric8.kubernetes.api.model.Namespaced; | ||
| import io.fabric8.kubernetes.client.CustomResource; | ||
| import io.fabric8.kubernetes.model.annotation.Group; | ||
| import io.fabric8.kubernetes.model.annotation.Kind; | ||
| import io.fabric8.kubernetes.model.annotation.Version; | ||
|
|
||
| @Group("sample.javaoperatorsdk") | ||
| @Version("v1") | ||
| @Kind("OwnerRefMultiVersionCR") | ||
| public class OwnerRefMultiVersionCR1 | ||
| extends CustomResource<OwnerRefMultiVersionSpec, OwnerRefMultiVersionStatus> | ||
| implements Namespaced {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /* | ||
| * Copyright Java Operator SDK Authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion; | ||
|
|
||
| import io.fabric8.kubernetes.api.model.Namespaced; | ||
| import io.fabric8.kubernetes.client.CustomResource; | ||
| import io.fabric8.kubernetes.model.annotation.Group; | ||
| import io.fabric8.kubernetes.model.annotation.Kind; | ||
| import io.fabric8.kubernetes.model.annotation.Version; | ||
|
|
||
| @Group("sample.javaoperatorsdk") | ||
| @Version(value = "v2", storage = false) | ||
| @Kind("OwnerRefMultiVersionCR") | ||
| public class OwnerRefMultiVersionCR2 | ||
| extends CustomResource<OwnerRefMultiVersionSpec, OwnerRefMultiVersionStatus> | ||
| implements Namespaced {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| /* | ||
| * Copyright Java Operator SDK Authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.extension.RegisterExtension; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import io.fabric8.kubernetes.api.model.ConfigMap; | ||
| import io.fabric8.kubernetes.api.model.ObjectMeta; | ||
| import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionVersion; | ||
| import io.fabric8.kubernetes.client.KubernetesClient; | ||
| import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.awaitility.Awaitility.await; | ||
|
|
||
| /** | ||
| * Integration test verifying that {@code Mappers.fromOwnerReferences} correctly maps secondary | ||
| * resources back to primary resources even after a CRD version change. The mapper compares only the | ||
| * group part of the apiVersion (ignoring the version), so owner references created under v1 should | ||
| * still work when the CRD storage version switches to v2. | ||
| */ | ||
| class OwnerRefMultiVersionIT { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(OwnerRefMultiVersionIT.class); | ||
|
|
||
| private static final String CR_NAME = "test-ownerref-mv"; | ||
| private static final String CRD_NAME = "ownerrefmultiversioncrs.sample.javaoperatorsdk"; | ||
|
|
||
| @RegisterExtension | ||
| LocallyRunOperatorExtension operator = | ||
| LocallyRunOperatorExtension.builder() | ||
| .withReconciler(new OwnerRefMultiVersionReconciler()) | ||
| .withBeforeStartHook( | ||
| ext -> { | ||
| // The auto-generated CRD has both v1 (storage) and v2. Remove v2 so the | ||
| // cluster initially only knows about v1. | ||
| var client = ext.getKubernetesClient(); | ||
| var crd = | ||
| client | ||
| .apiextensions() | ||
| .v1() | ||
| .customResourceDefinitions() | ||
| .withName(CRD_NAME) | ||
| .get(); | ||
| if (crd != null) { | ||
| crd.getSpec().getVersions().removeIf(v -> "v2".equals(v.getName())); | ||
| crd.getMetadata().setResourceVersion(null); | ||
| crd.getMetadata().setManagedFields(null); | ||
| client.resource(crd).serverSideApply(); | ||
| log.info("Applied CRD with v1 only"); | ||
| } | ||
| }) | ||
| .build(); | ||
|
|
||
| @Test | ||
| void mapperWorksAcrossVersionChange() { | ||
| var reconciler = operator.getReconcilerOfType(OwnerRefMultiVersionReconciler.class); | ||
|
|
||
| // 1. Create a v1 custom resource | ||
| var cr = createCR(); | ||
| operator.create(cr); | ||
|
|
||
| // 2. Wait for initial reconciliation: ConfigMap is created with owner ref to v1 | ||
| await() | ||
| .atMost(Duration.ofSeconds(30)) | ||
| .pollInterval(Duration.ofMillis(300)) | ||
| .untilAsserted( | ||
| () -> { | ||
| var current = operator.get(OwnerRefMultiVersionCR1.class, CR_NAME); | ||
| assertThat(current.getStatus()).isNotNull(); | ||
| assertThat(current.getStatus().getReconcileCount()).isPositive(); | ||
|
|
||
| var cm = operator.get(ConfigMap.class, CR_NAME); | ||
| assertThat(cm).isNotNull(); | ||
| assertThat(cm.getMetadata().getOwnerReferences()).hasSize(1); | ||
| assertThat(cm.getMetadata().getOwnerReferences().get(0).getApiVersion()) | ||
| .isEqualTo("sample.javaoperatorsdk/v1"); | ||
| }); | ||
|
|
||
| int countBeforeUpdate = reconciler.getReconcileCount(); | ||
| log.info("Reconcile count before CRD update: {}", countBeforeUpdate); | ||
|
|
||
| // 3. Update CRD to add v2 as the new storage version | ||
| updateCrdWithV2AsStorage(operator.getKubernetesClient()); | ||
|
|
||
| // 4. Modify the ConfigMap to trigger the informer event source. | ||
| // The mapper should still map the ConfigMap (with v1 owner ref) to the primary CR. | ||
| var cm = operator.get(ConfigMap.class, CR_NAME); | ||
| cm.getData().put("updated", "true"); | ||
| operator.replace(cm); | ||
|
|
||
| // 5. Verify reconciliation was triggered again | ||
| await() | ||
| .atMost(Duration.ofSeconds(30)) | ||
| .pollInterval(Duration.ofMillis(300)) | ||
| .untilAsserted( | ||
| () -> { | ||
| assertThat(reconciler.getReconcileCount()).isGreaterThan(countBeforeUpdate); | ||
| }); | ||
|
|
||
| log.info("Reconcile count after CRD update: {}", reconciler.getReconcileCount()); | ||
| } | ||
|
|
||
| private void updateCrdWithV2AsStorage(KubernetesClient client) { | ||
| var crd = client.apiextensions().v1().customResourceDefinitions().withName(CRD_NAME).get(); | ||
|
|
||
| // Set v1 to non-storage | ||
| for (var version : crd.getSpec().getVersions()) { | ||
| if ("v1".equals(version.getName())) { | ||
| version.setStorage(false); | ||
| } | ||
| } | ||
|
|
||
| // Add v2 as storage version, reusing v1's schema (specs are compatible) | ||
| var v1 = | ||
| crd.getSpec().getVersions().stream() | ||
| .filter(v -> "v1".equals(v.getName())) | ||
| .findFirst() | ||
| .orElseThrow(); | ||
|
|
||
| var v2 = new CustomResourceDefinitionVersion(); | ||
| v2.setName("v2"); | ||
| v2.setServed(true); | ||
| v2.setStorage(true); | ||
| v2.setSchema(v1.getSchema()); | ||
| v2.setSubresources(v1.getSubresources()); | ||
| crd.getSpec().getVersions().add(v2); | ||
|
|
||
| crd.getMetadata().setResourceVersion(null); | ||
| crd.getMetadata().setManagedFields(null); | ||
| client.resource(crd).serverSideApply(); | ||
| log.info("Updated CRD with v2 as storage version"); | ||
| } | ||
|
|
||
| private OwnerRefMultiVersionCR1 createCR() { | ||
| var cr = new OwnerRefMultiVersionCR1(); | ||
| cr.setMetadata(new ObjectMeta()); | ||
| cr.getMetadata().setName(CR_NAME); | ||
| cr.setSpec(new OwnerRefMultiVersionSpec()); | ||
| cr.getSpec().setValue("initial"); | ||
| return cr; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method should probably eventually move to the client code itself