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
61 changes: 60 additions & 1 deletion docs/content/en/docs/documentation/reconciler.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,63 @@ In this mode:
- you cannot use managed dependent resources since those manage the finalizers and other logic related to the normal
execution mode.

See also [sample](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling) for selectively adding finalizers for resources;
See also [sample](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling) for selectively adding finalizers for resources;

### Expectations

Expectations are a pattern to make sure to check in the reconciliation that your secondary resources are in a certain state.
For a more detailed explanation see [this blogpost](https://ahmet.im/blog/controller-pitfalls/#expectations-pattern).
You can find framework support for this pattern in [`io.javaoperatorsdk.operator.processing.expectation`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/expectation/)
package. See also related [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/expectation/ExpectationReconciler.java).
Note that this feature is marked as `@Experimental`, since based on feedback the API might be improved / changed, but we intend
to support it, later also might be integrated to Dependent Resources and/or Workflows.

The idea is the nutshell, is that you can track your expectations in the expectation manager in the reconciler.
Which has an api that covers the common use cases.

The following sample is the simplified version of the integration tests that implements a logic that creates a
deployment and sets status message if there are the target three replicas ready:

```java
public class ExpectationReconciler implements Reconciler<ExpectationCustomResource> {

// some code is omitted

private final ExpectationManager<ExpectationCustomResource> expectationManager =
new ExpectationManager<>();

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

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

var deployment = context.getSecondaryResource(Deployment.class);
if (deployment.isEmpty()) {
createDeployment(primary, context);
expectationManager.setExpectation(
primary, Duration.ofSeconds(timeout), deploymentReadyExpectation(context));
return UpdateControl.noUpdate();
} else {
// checks the expectation if it is fulfilled also removes it,
// in your logic you might add a next expectation based on your workflow.
// Expectations have a name, so you can easily distinguish them if there is more of them.
var res = expectationManager.checkExpectation("deploymentReadyExpectation",primary, context);
if (res.isFulfilled()) {
return pathchStatusWithMessage(primary, DEPLOYMENT_READY);
} else if (res.isTimedOut()) {
// you might add some other timeout handling here
return pathchStatusWithMessage(primary, DEPLOYMENT_TIMEOUT);
}
}
return UpdateControl.noUpdate();

}
}
```



Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.PACKAGE})
public @interface Experimental {
/**
* Message for experimental features that we intend to keep and maintain, but the API might change
* usually, based on user feedback.
*/
String API_MIGHT_CHANGE = "API might change, usually based on feedback";

/**
* Describes why the annotated element is experimental.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.processing.expectation;

import java.util.function.BiPredicate;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.Experimental;

import static io.javaoperatorsdk.operator.api.reconciler.Experimental.API_MIGHT_CHANGE;

@Experimental(API_MIGHT_CHANGE)
public interface Expectation<P extends HasMetadata> {

String UNNAMED = "unnamed";

boolean isFulfilled(P primary, Context<P> context);

default String name() {
return UNNAMED;
}

static <P extends HasMetadata> Expectation<P> createExpectation(
String name, BiPredicate<P, Context<P>> predicate) {
return new Expectation<>() {
@Override
public String name() {
return name;
}

@Override
public boolean isFulfilled(P primary, Context<P> context) {
return predicate.test(primary, context);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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.processing.expectation;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.Experimental;
import io.javaoperatorsdk.operator.processing.event.ResourceID;

import static io.javaoperatorsdk.operator.api.reconciler.Experimental.API_MIGHT_CHANGE;

@Experimental(API_MIGHT_CHANGE)
public class ExpectationManager<P extends HasMetadata> {

protected final ConcurrentHashMap<ResourceID, RegisteredExpectation<P>> registeredExpectations =
new ConcurrentHashMap<>();

/**
* Checks if the expectation holds, if not sets the expectation with the given timeout.
*
* @return false, if the expectation is already fulfilled, therefore, not registered. Returns true
* if expectation is not met and set with a timeout.
*/
public boolean checkAndSetExpectation(
P primary, Context<P> context, Duration timeout, Expectation<P> expectation) {
var fulfilled = expectation.isFulfilled(primary, context);
if (fulfilled) {
return false;
} else {
setExpectation(primary, timeout, expectation);
return true;
}
}

/**
* Sets a target expectation with given timeout.
*
* @param primary resource
* @param timeout of expectation
* @param expectation to check
*/
// we might consider in the future to throw an exception if an expectation is already set
public void setExpectation(P primary, Duration timeout, Expectation<P> expectation) {
registeredExpectations.put(
ResourceID.fromResource(primary),
new RegisteredExpectation<>(LocalDateTime.now(), timeout, expectation));
}

/**
* Checks on expectation with provided name. Return the expectation result. If the result of
* expectation is fulfilled, the expectation is automatically removed;
*/
public ExpectationResult<P> checkExpectation(
String expectationName, P primary, Context<P> context) {
var resourceID = ResourceID.fromResource(primary);
var exp = registeredExpectations.get(ResourceID.fromResource(primary));
if (exp != null && expectationName.equals(exp.expectation().name())) {
return checkExpectation(exp, resourceID, primary, context);
} else {
return checkExpectation(null, resourceID, primary, context);
}
}

/**
* Checks if actual expectation is fulfilled. Return the expectation result. If the result of
* expectation is fulfilled, the expectation is automatically removed;
*/
public ExpectationResult<P> checkExpectation(P primary, Context<P> context) {
var resourceID = ResourceID.fromResource(primary);
var exp = registeredExpectations.get(ResourceID.fromResource(primary));
return checkExpectation(exp, resourceID, primary, context);
}

private ExpectationResult<P> checkExpectation(
RegisteredExpectation<P> exp, ResourceID resourceID, P primary, Context<P> context) {
if (exp == null) {
return new ExpectationResult<>(null, null);
}
if (exp.expectation().isFulfilled(primary, context)) {
registeredExpectations.remove(resourceID);
return new ExpectationResult<>(exp.expectation(), ExpectationStatus.FULFILLED);
} else if (exp.isTimedOut()) {
// we don't remove the expectation so user knows about it's state
return new ExpectationResult<>(exp.expectation(), ExpectationStatus.TIMED_OUT);
} else {
return new ExpectationResult<>(exp.expectation(), ExpectationStatus.NOT_YET_FULFILLED);
}
}

/*
* Returns true if there is an expectation for the primary resource, but it is not yet fulfilled
* neither timed out.
* The intention behind is that you can exit reconciliation early with a simple check
* if true.
* */
public boolean ongoingExpectationPresent(P primary, Context<P> context) {
var exp = registeredExpectations.get(ResourceID.fromResource(primary));
if (exp == null) {
return false;
}
return !exp.isTimedOut() && !exp.expectation().isFulfilled(primary, context);
}

public boolean isExpectationPresent(P primary) {
return registeredExpectations.containsKey(ResourceID.fromResource(primary));
}

public boolean isExpectationPresent(String name, P primary) {
var exp = registeredExpectations.get(ResourceID.fromResource(primary));
return exp != null && name.equals(exp.expectation().name());
}

public Optional<Expectation<P>> getExpectation(P primary) {
var regExp = registeredExpectations.get(ResourceID.fromResource(primary));
return Optional.ofNullable(regExp).map(RegisteredExpectation::expectation);
}

public Optional<String> getExpectationName(P primary) {
return getExpectation(primary).map(Expectation::name);
}

public void removeExpectation(P primary) {
registeredExpectations.remove(ResourceID.fromResource(primary));
}

public void cleanup(P primary) {
registeredExpectations.remove(ResourceID.fromResource(primary));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.processing.expectation;

import io.fabric8.kubernetes.api.model.HasMetadata;

public record ExpectationResult<P extends HasMetadata>(
Expectation<P> expectation, ExpectationStatus status) {

public boolean isFulfilled() {
return status == ExpectationStatus.FULFILLED;
}

public boolean isTimedOut() {
return status == ExpectationStatus.TIMED_OUT;
}

public boolean isExpectationPresent() {
return expectation != null;
}

public boolean isNotPresentOrFulfilled() {
return !isExpectationPresent() || isFulfilled();
}

public String name() {
return expectation.name();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.processing.expectation;

public enum ExpectationStatus {
FULFILLED,
NOT_YET_FULFILLED,
TIMED_OUT
}
Loading