Skip to content

Commit a002783

Browse files
committed
integration test skeleton
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent e57272d commit a002783

6 files changed

Lines changed: 372 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion;
17+
18+
import io.fabric8.kubernetes.api.model.Namespaced;
19+
import io.fabric8.kubernetes.client.CustomResource;
20+
import io.fabric8.kubernetes.model.annotation.Group;
21+
import io.fabric8.kubernetes.model.annotation.Kind;
22+
import io.fabric8.kubernetes.model.annotation.Version;
23+
24+
@Group("sample.javaoperatorsdk")
25+
@Version("v1")
26+
@Kind("OwnerRefMultiVersionCR")
27+
public class OwnerRefMultiVersionCR1
28+
extends CustomResource<OwnerRefMultiVersionSpec, OwnerRefMultiVersionStatus>
29+
implements Namespaced {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion;
17+
18+
import io.fabric8.kubernetes.api.model.Namespaced;
19+
import io.fabric8.kubernetes.client.CustomResource;
20+
import io.fabric8.kubernetes.model.annotation.Group;
21+
import io.fabric8.kubernetes.model.annotation.Kind;
22+
import io.fabric8.kubernetes.model.annotation.Version;
23+
24+
@Group("sample.javaoperatorsdk")
25+
@Version(value = "v2", storage = false)
26+
@Kind("OwnerRefMultiVersionCR")
27+
public class OwnerRefMultiVersionCR2
28+
extends CustomResource<OwnerRefMultiVersionSpec, OwnerRefMultiVersionStatus>
29+
implements Namespaced {}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion;
17+
18+
import java.time.Duration;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.RegisterExtension;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
import io.fabric8.kubernetes.api.model.ConfigMap;
26+
import io.fabric8.kubernetes.api.model.ObjectMeta;
27+
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionVersion;
28+
import io.fabric8.kubernetes.client.KubernetesClient;
29+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.awaitility.Awaitility.await;
33+
34+
/**
35+
* Integration test verifying that {@code Mappers.fromOwnerReferences} correctly maps secondary
36+
* resources back to primary resources even after a CRD version change. The mapper compares only the
37+
* group part of the apiVersion (ignoring the version), so owner references created under v1 should
38+
* still work when the CRD storage version switches to v2.
39+
*/
40+
class OwnerRefMultiVersionIT {
41+
42+
private static final Logger log = LoggerFactory.getLogger(OwnerRefMultiVersionIT.class);
43+
44+
private static final String CR_NAME = "test-ownerref-mv";
45+
private static final String CRD_NAME = "ownerrefmultiversioncrs.sample.javaoperatorsdk";
46+
47+
@RegisterExtension
48+
LocallyRunOperatorExtension operator =
49+
LocallyRunOperatorExtension.builder()
50+
.withReconciler(new OwnerRefMultiVersionReconciler())
51+
.withBeforeStartHook(
52+
ext -> {
53+
// The auto-generated CRD has both v1 (storage) and v2. Remove v2 so the
54+
// cluster initially only knows about v1.
55+
var client = ext.getKubernetesClient();
56+
var crd =
57+
client
58+
.apiextensions()
59+
.v1()
60+
.customResourceDefinitions()
61+
.withName(CRD_NAME)
62+
.get();
63+
if (crd != null) {
64+
crd.getSpec().getVersions().removeIf(v -> "v2".equals(v.getName()));
65+
crd.getMetadata().setResourceVersion(null);
66+
crd.getMetadata().setManagedFields(null);
67+
client.resource(crd).serverSideApply();
68+
log.info("Applied CRD with v1 only");
69+
}
70+
})
71+
.build();
72+
73+
@Test
74+
void mapperWorksAcrossVersionChange() {
75+
var reconciler = operator.getReconcilerOfType(OwnerRefMultiVersionReconciler.class);
76+
77+
// 1. Create a v1 custom resource
78+
var cr = createCR();
79+
operator.create(cr);
80+
81+
// 2. Wait for initial reconciliation: ConfigMap is created with owner ref to v1
82+
await()
83+
.atMost(Duration.ofSeconds(30))
84+
.pollInterval(Duration.ofMillis(300))
85+
.untilAsserted(
86+
() -> {
87+
var current = operator.get(OwnerRefMultiVersionCR1.class, CR_NAME);
88+
assertThat(current.getStatus()).isNotNull();
89+
assertThat(current.getStatus().getReconcileCount()).isPositive();
90+
91+
var cm = operator.get(ConfigMap.class, CR_NAME);
92+
assertThat(cm).isNotNull();
93+
assertThat(cm.getMetadata().getOwnerReferences()).hasSize(1);
94+
assertThat(cm.getMetadata().getOwnerReferences().get(0).getApiVersion())
95+
.isEqualTo("sample.javaoperatorsdk/v1");
96+
});
97+
98+
int countBeforeUpdate = reconciler.getReconcileCount();
99+
log.info("Reconcile count before CRD update: {}", countBeforeUpdate);
100+
101+
// 3. Update CRD to add v2 as the new storage version
102+
updateCrdWithV2AsStorage(operator.getKubernetesClient());
103+
104+
// 4. Modify the ConfigMap to trigger the informer event source.
105+
// The mapper should still map the ConfigMap (with v1 owner ref) to the primary CR.
106+
var cm = operator.get(ConfigMap.class, CR_NAME);
107+
cm.getData().put("updated", "true");
108+
operator.replace(cm);
109+
110+
// 5. Verify reconciliation was triggered again
111+
await()
112+
.atMost(Duration.ofSeconds(30))
113+
.pollInterval(Duration.ofMillis(300))
114+
.untilAsserted(
115+
() -> {
116+
assertThat(reconciler.getReconcileCount()).isGreaterThan(countBeforeUpdate);
117+
});
118+
119+
log.info("Reconcile count after CRD update: {}", reconciler.getReconcileCount());
120+
}
121+
122+
private void updateCrdWithV2AsStorage(KubernetesClient client) {
123+
var crd = client.apiextensions().v1().customResourceDefinitions().withName(CRD_NAME).get();
124+
125+
// Set v1 to non-storage
126+
for (var version : crd.getSpec().getVersions()) {
127+
if ("v1".equals(version.getName())) {
128+
version.setStorage(false);
129+
}
130+
}
131+
132+
// Add v2 as storage version, reusing v1's schema (specs are compatible)
133+
var v1 =
134+
crd.getSpec().getVersions().stream()
135+
.filter(v -> "v1".equals(v.getName()))
136+
.findFirst()
137+
.orElseThrow();
138+
139+
var v2 = new CustomResourceDefinitionVersion();
140+
v2.setName("v2");
141+
v2.setServed(true);
142+
v2.setStorage(true);
143+
v2.setSchema(v1.getSchema());
144+
v2.setSubresources(v1.getSubresources());
145+
crd.getSpec().getVersions().add(v2);
146+
147+
crd.getMetadata().setResourceVersion(null);
148+
crd.getMetadata().setManagedFields(null);
149+
client.resource(crd).serverSideApply();
150+
log.info("Updated CRD with v2 as storage version");
151+
}
152+
153+
private OwnerRefMultiVersionCR1 createCR() {
154+
var cr = new OwnerRefMultiVersionCR1();
155+
cr.setMetadata(new ObjectMeta());
156+
cr.getMetadata().setName(CR_NAME);
157+
cr.setSpec(new OwnerRefMultiVersionSpec());
158+
cr.getSpec().setValue("initial");
159+
return cr;
160+
}
161+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.concurrent.atomic.AtomicInteger;
21+
22+
import io.fabric8.kubernetes.api.model.ConfigMap;
23+
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
24+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
25+
import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
26+
import io.javaoperatorsdk.operator.api.reconciler.Context;
27+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
28+
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
29+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
30+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
31+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
32+
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
33+
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
34+
35+
@ControllerConfiguration
36+
public class OwnerRefMultiVersionReconciler implements Reconciler<OwnerRefMultiVersionCR1> {
37+
38+
public static final String LABEL_KEY = "ownerref-multiversion-test";
39+
public static final String LABEL_VALUE = "true";
40+
public static final String DATA_KEY = "data";
41+
42+
private final AtomicInteger reconcileCount = new AtomicInteger(0);
43+
44+
@Override
45+
public UpdateControl<OwnerRefMultiVersionCR1> reconcile(
46+
OwnerRefMultiVersionCR1 resource, Context<OwnerRefMultiVersionCR1> context) {
47+
48+
var client = context.getClient();
49+
var configMapName = resource.getMetadata().getName();
50+
var namespace = resource.getMetadata().getNamespace();
51+
52+
var existingCM = client.configMaps().inNamespace(namespace).withName(configMapName).get();
53+
if (existingCM == null) {
54+
var cm =
55+
new ConfigMapBuilder()
56+
.withMetadata(
57+
new ObjectMetaBuilder()
58+
.withName(configMapName)
59+
.withNamespace(namespace)
60+
.withLabels(Map.of(LABEL_KEY, LABEL_VALUE))
61+
.build())
62+
.withData(Map.of(DATA_KEY, resource.getSpec().getValue()))
63+
.build();
64+
cm.addOwnerReference(resource);
65+
client.configMaps().resource(cm).create();
66+
}
67+
68+
int count = reconcileCount.incrementAndGet();
69+
if (resource.getStatus() == null) {
70+
resource.setStatus(new OwnerRefMultiVersionStatus());
71+
}
72+
resource.getStatus().setReconcileCount(count);
73+
return UpdateControl.patchStatus(resource);
74+
}
75+
76+
@Override
77+
public List<EventSource<?, OwnerRefMultiVersionCR1>> prepareEventSources(
78+
EventSourceContext<OwnerRefMultiVersionCR1> context) {
79+
var ies =
80+
new InformerEventSource<>(
81+
InformerEventSourceConfiguration.from(ConfigMap.class, OwnerRefMultiVersionCR1.class)
82+
.withSecondaryToPrimaryMapper(
83+
Mappers.fromOwnerReferences(context.getPrimaryResourceClass()))
84+
.withLabelSelector(LABEL_KEY + "=" + LABEL_VALUE)
85+
.build(),
86+
context);
87+
return List.of(ies);
88+
}
89+
90+
public int getReconcileCount() {
91+
return reconcileCount.get();
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion;
17+
18+
public class OwnerRefMultiVersionSpec {
19+
20+
private String value;
21+
22+
public String getValue() {
23+
return value;
24+
}
25+
26+
public OwnerRefMultiVersionSpec setValue(String value) {
27+
this.value = value;
28+
return this;
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion;
17+
18+
public class OwnerRefMultiVersionStatus {
19+
20+
private int reconcileCount;
21+
22+
public int getReconcileCount() {
23+
return reconcileCount;
24+
}
25+
26+
public OwnerRefMultiVersionStatus setReconcileCount(int reconcileCount) {
27+
this.reconcileCount = reconcileCount;
28+
return this;
29+
}
30+
}

0 commit comments

Comments
 (0)