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

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
@@ -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.expectation;

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.ShortNames;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("sample.javaoperatorsdk")
@Version("v1")
@ShortNames("pcecr")
public class PeriodicCleanerExpectationCustomResource
extends CustomResource<Void, PeriodicCleanerExpectationCustomResourceStatus>
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.expectation;

public class PeriodicCleanerExpectationCustomResourceStatus {

private String message;

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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.expectation;

import java.time.Duration;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;

import static io.javaoperatorsdk.operator.baseapi.expectation.PeriodicCleanerExpectationReconciler.DEPLOYMENT_READY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

/**
* Integration test showcasing PeriodicCleanerExpectationManager usage.
*
* <p>This test demonstrates the key benefits of PeriodicCleanerExpectationManager: 1. Works without
* requiring @ControllerConfiguration(triggerReconcilerOnAllEvents = true) 2. Automatically cleans
* up stale expectations periodically 3. Maintains the same expectation API and functionality as the
* regular ExpectationManager
*/
class PeriodicCleanerExpectationIT {

public static final String TEST_1 = "test1";
public static final String TEST_2 = "test2";

@RegisterExtension
LocallyRunOperatorExtension extension =
LocallyRunOperatorExtension.builder()
.withReconciler(new PeriodicCleanerExpectationReconciler())
.build();

@Test
void testPeriodicCleanerExpectationBasicFlow() {
extension
.getReconcilerOfType(PeriodicCleanerExpectationReconciler.class)
.setTimeout(Duration.ofSeconds(30));
var res = testResource();
extension.create(res);

await()
.untilAsserted(
() -> {
var actual = extension.get(PeriodicCleanerExpectationCustomResource.class, TEST_1);
assertThat(actual.getStatus()).isNotNull();
assertThat(actual.getStatus().getMessage()).isEqualTo(DEPLOYMENT_READY);
});
}

@Test
void testPeriodicCleanerExpectationTimeouts() {
extension
.getReconcilerOfType(PeriodicCleanerExpectationReconciler.class)
.setTimeout(Duration.ofMillis(300));
var res = testResource();
extension.create(res);

await()
.untilAsserted(
() -> {
var actual = extension.get(PeriodicCleanerExpectationCustomResource.class, TEST_1);
assertThat(actual.getStatus()).isNotNull();
assertThat(actual.getStatus().getMessage()).isEqualTo(DEPLOYMENT_READY);
});
}

@Test
void demonstratesNoTriggerReconcilerOnAllEventsNeeded() {
// This test demonstrates that PeriodicCleanerExpectationManager works
// without @ControllerConfiguration(triggerReconcilerOnAllEvents = true)

// The PeriodicCleanerExpectationReconciler doesn't use triggerReconcilerOnAllEvents = true
// yet expectations still work properly due to the periodic cleanup functionality

var reconciler = extension.getReconcilerOfType(PeriodicCleanerExpectationReconciler.class);
reconciler.setTimeout(Duration.ofSeconds(30));

var res = testResource("no-trigger-test");
extension.create(res);

// Verify that expectations work even without triggerReconcilerOnAllEvents = true
await()
.untilAsserted(
() -> {
var actual =
extension.get(PeriodicCleanerExpectationCustomResource.class, "no-trigger-test");
assertThat(actual.getStatus()).isNotNull();
assertThat(actual.getStatus().getMessage()).isEqualTo(DEPLOYMENT_READY);
});
}

private PeriodicCleanerExpectationCustomResource testResource() {
return testResource(TEST_1);
}

private PeriodicCleanerExpectationCustomResource testResource(String name) {
var res = new PeriodicCleanerExpectationCustomResource();
res.setMetadata(new ObjectMetaBuilder().withName(name).build());
return res;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* 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.expectation;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import io.fabric8.kubernetes.api.model.ContainerBuilder;
import io.fabric8.kubernetes.api.model.ContainerPortBuilder;
import io.fabric8.kubernetes.api.model.LabelSelectorBuilder;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder;
import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
import io.javaoperatorsdk.operator.processing.expectation.Expectation;
import io.javaoperatorsdk.operator.processing.expectation.PeriodicCleanerExpectationManager;

/**
* Integration test reconciler showcasing PeriodicCleanerExpectationManager usage.
*
* <p>Key differences from ExpectationReconciler: - Uses PeriodicCleanerExpectationManager instead
* of ExpectationManager - Does NOT use @ControllerConfiguration(triggerReconcilerOnAllEvents =
* true) - Demonstrates periodic cleanup functionality without requiring reconciler triggers on all
* events
*/
@ControllerConfiguration
public class PeriodicCleanerExpectationReconciler
implements Reconciler<PeriodicCleanerExpectationCustomResource> {

public static final String DEPLOYMENT_READY = "Deployment ready";
public static final String DEPLOYMENT_TIMEOUT = "Deployment timeout";
public static final String DEPLOYMENT_READY_EXPECTATION_NAME = "deploymentReadyExpectation";

private PeriodicCleanerExpectationManager<PeriodicCleanerExpectationCustomResource>
expectationManager;
private final AtomicReference<Duration> timeoutRef =
new AtomicReference<>(Duration.ofSeconds(30));

public PeriodicCleanerExpectationReconciler() {
// expectationManager will be initialized in prepareEventSources when cache is available
}

public void setTimeout(Duration timeout) {
timeoutRef.set(timeout);
}

public PeriodicCleanerExpectationManager<PeriodicCleanerExpectationCustomResource>
getExpectationManager() {
return expectationManager;
}

@Override
public UpdateControl<PeriodicCleanerExpectationCustomResource> reconcile(
PeriodicCleanerExpectationCustomResource primary,
Context<PeriodicCleanerExpectationCustomResource> context) {

// Note: Unlike regular ExpectationManager, we don't need to manually clean up on delete
// because PeriodicCleanerExpectationManager handles this automatically via periodic cleanup

// exiting asap if there is an expectation that is not timed out neither fulfilled
if (expectationManager.ongoingExpectationPresent(primary, context)) {
return UpdateControl.noUpdate();
}

var deployment = context.getSecondaryResource(Deployment.class);
if (deployment.isEmpty()) {
createDeployment(primary, context);
var set =
expectationManager.checkAndSetExpectation(
primary, context, timeoutRef.get(), deploymentReadyExpectation());
if (set) {
return UpdateControl.noUpdate();
}
} else {
// Checks the expectation and removes it once it is fulfilled.
// In your logic you might add a next expectation based on your workflow.
// Expectations have a name, so you can easily distinguish multiple expectations.
var res =
expectationManager.checkExpectation(DEPLOYMENT_READY_EXPECTATION_NAME, primary, context);
// note that this happens only once, since if the expectation is fulfilled, it is also removed
// from the manager
if (res.isFulfilled()) {
return patchStatusWithMessage(primary, DEPLOYMENT_READY);
} else if (res.isTimedOut()) {
// you might add some other timeout handling here
return patchStatusWithMessage(primary, DEPLOYMENT_TIMEOUT);
}
}
return UpdateControl.noUpdate();
}

@Override
public List<EventSource<?, PeriodicCleanerExpectationCustomResource>> prepareEventSources(
EventSourceContext<PeriodicCleanerExpectationCustomResource> context) {

// Initialize expectationManager with primary cache from the context
// Use a short period (1 second) for faster testing
var primaryCache = context.getPrimaryCache();
this.expectationManager =
new PeriodicCleanerExpectationManager<>(Duration.ofSeconds(1), primaryCache);

return List.of(
new InformerEventSource<>(
InformerEventSourceConfiguration.from(
Deployment.class, PeriodicCleanerExpectationCustomResource.class)
.build(),
context));
}

private static void createDeployment(
PeriodicCleanerExpectationCustomResource primary,
Context<PeriodicCleanerExpectationCustomResource> context) {
var deployment =
new DeploymentBuilder()
.withMetadata(
new ObjectMetaBuilder()
.withName(primary.getMetadata().getName())
.withNamespace(primary.getMetadata().getNamespace())
.build())
.withSpec(
new DeploymentSpecBuilder()
.withReplicas(3)
.withSelector(
new LabelSelectorBuilder().withMatchLabels(Map.of("app", "nginx")).build())
.withTemplate(
new PodTemplateSpecBuilder()
.withMetadata(
new ObjectMetaBuilder().withLabels(Map.of("app", "nginx")).build())
.withSpec(
new PodSpecBuilder()
.withContainers(
new ContainerBuilder()
.withName("nginx")
.withImage("nginx:1.29.2")
.withPorts(
new ContainerPortBuilder()
.withContainerPort(80)
.build())
.build())
.build())
.build())
.build())
.build();
deployment.addOwnerReference(primary);
context.getClient().resource(deployment).serverSideApply();
}

private static Expectation<PeriodicCleanerExpectationCustomResource>
deploymentReadyExpectation() {
return Expectation.createExpectation(
DEPLOYMENT_READY_EXPECTATION_NAME,
(primary, context) ->
context
.getSecondaryResource(Deployment.class)
.map(
ad ->
ad.getStatus() != null
&& ad.getStatus().getReadyReplicas() != null
&& ad.getStatus().getReadyReplicas() == 3)
.orElse(false));
}

private static UpdateControl<PeriodicCleanerExpectationCustomResource> patchStatusWithMessage(
PeriodicCleanerExpectationCustomResource primary, String message) {
primary.setStatus(new PeriodicCleanerExpectationCustomResourceStatus());
primary.getStatus().setMessage(message);
return UpdateControl.patchStatus(primary);
}
}