Description
Bug Report
What did you do?
I'm reconciling a standalone dependent resource of type Secret that needs to be updated (it exists in k8s but I need to update it with new data).
I believe this also affects managed dependent resources, and any type of resource.
What did you expect to see?
During the reconcile()
method, the desired()
method should have been called only once.
What did you see instead? Under which circumstances?
During the reconcile()
method, the desired()
method is called twice.
Environment
Kubernetes cluster type:
OpenShift
$ Mention java-operator-sdk version from pom.xml file
4.2.2
$ java -version
openjdk version "11.0.16.1" 2022-08-12
OpenJDK Runtime Environment Temurin-11.0.16.1+1 (build 11.0.16.1+1)
OpenJDK 64-Bit Server VM Temurin-11.0.16.1+1 (build 11.0.16.1+1, mixed mode)
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"26", GitVersion:"v1.26.0", GitCommit:"b46a3f887ca979b1a5d14fd39cb1af43e7e5d12d", GitTreeState:"clean", BuildDate:"2022-12-08T19:58:30Z", GoVersion:"go1.19.4", Compiler:"gc", Platform:"linux/amd64"}
Kustomize Version: v4.5.7
Server Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.5+70fb84c", GitCommit:"3c28e7a79b58e78b4c1dc1ab7e5f6c6c2d3aedd3", GitTreeState:"clean", BuildDate:"2022-04-25T15:58:12Z", GoVersion:"go1.17.5", Compiler:"gc", Platform:"linux/amd64"}
Possible Solution
In the reconcile()
method of AbstractDependentResource
, at line 67, the match()
method calls desired()
and stores the computed actual resources in the Matcher.Result
object (which is good).
https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java#L67
But then, in line 69, desired()
is called AGAIN (even though it's in orElse
part):
https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java#L69
Line 69 should change and only invoke desired()
if the Optional match.computedDesired()
is empty.
Simple change :)
Here's the current code:
67 final Matcher.Result<R> match = match(actualResource, primary, context);
68 if (!match.matched()) {
69 final var desired = match.computedDesired().orElse(desired(primary, context));
throwIfNull(desired, primary, "Desired");
logForOperation("Updating", primary, desired);
var updatedResource = handleUpdate(actualResource, desired, primary, context);
return ReconcileResult.resourceUpdated(updatedResource);
}
Additional context
In my case, we're generating some random data, including passwords, that are stored in the secret. I've implemented a custom check to determine if the reconciliation of the secret is required (basically, if it does not exist or it is empty), and only in such a case generate the passwords and create/update the secret with the random passwords. I observed that when the secret exists but is empty, passwords were actually generated twice before being applied to the secret. The first set of passwords were actually used, and the second was generated for nothing (since it's in the orElse
part that is not used since the desired state was already computed).