diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b81fa41b6d..e37ab5d8d5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -11,7 +11,10 @@ jobs:
strategy:
matrix:
java: [ 17, 21, 24 ]
- kubernetes: [ '1.30.12', '1.31.8', '1.32.4','1.33.0' ]
+ # Use the latest versions supported by minikube, otherwise GitHub it will
+ # end up in a throttling requests from minikube and workflow will fail.
+ # Minikube does such requests only if a version is not officially supported.
+ kubernetes: [ '1.30.12', '1.31.8', '1.32.4','1.33.1' ]
uses: ./.github/workflows/integration-tests.yml
with:
java-version: ${{ matrix.java }}
@@ -35,9 +38,9 @@ jobs:
matrix:
java: [ 17, 21, 24 ]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: ${{ matrix.java }}
diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml
index e06b427960..f72a0af43b 100644
--- a/.github/workflows/e2e-test.yml
+++ b/.github/workflows/e2e-test.yml
@@ -1,6 +1,6 @@
# Integration and end to end tests which runs locally and deploys the Operator to a Kubernetes
# (Minikube) cluster and creates custom resources to verify the operator's functionality
-name: Integration & End to End tests
+name: End to End tests
on:
pull_request:
paths-ignore:
@@ -27,18 +27,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Setup Minikube-Kubernetes
uses: manusa/actions-setup-minikube@v2.14.0
with:
- minikube version: v1.34.0
- kubernetes version: v1.33.0
+ minikube version: v1.36.0
+ # Use the latest versions supported by minikube, otherwise GitHub it will
+ # end up in a throttling requests from minikube and workflow will fail.
+ # Minikube does such requests only if a version is not officially supported.
+ kubernetes version: v1.33.1
github token: ${{ secrets.GITHUB_TOKEN }}
driver: docker
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin
diff --git a/.github/workflows/hugo.yaml b/.github/workflows/hugo.yaml
index 511f10a8e0..c1b967616d 100644
--- a/.github/workflows/hugo.yaml
+++ b/.github/workflows/hugo.yaml
@@ -41,7 +41,7 @@ jobs:
- name: Install Dart Sass
run: sudo snap install dart-sass
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
fetch-depth: 0
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 75a6093371..fdb8897c07 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -29,11 +29,11 @@ jobs:
continue-on-error: ${{ inputs.experimental }}
timeout-minutes: 40
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
ref: ${{ inputs.checkout-ref }}
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: ${{ inputs.java-version }}
@@ -41,10 +41,10 @@ jobs:
- name: Set up Minikube
uses: manusa/actions-setup-minikube@v2.14.0
with:
- minikube version: 'v1.34.0'
+ minikube version: 'v1.36.0'
kubernetes version: '${{ inputs.kube-version }}'
- driver: 'docker'
- github token: ${{ secrets.GITHUB_TOKEN }}
+ github token: ${{ github.token }}
+
- name: "${{inputs.it-category}} integration tests (kube: ${{ inputs.kube-version }} / java: ${{ inputs.java-version }} / client: ${{ inputs.http-client }})"
run: |
if [ -z "${{inputs.it-category}}" ]; then
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 27742bf7c2..df5819f33d 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -17,9 +17,9 @@ jobs:
check_format_and_unit_tests:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
diff --git a/.github/workflows/release-project-in-dir.yml b/.github/workflows/release-project-in-dir.yml
index dc79b6f6c2..0683b01b1f 100644
--- a/.github/workflows/release-project-in-dir.yml
+++ b/.github/workflows/release-project-in-dir.yml
@@ -19,16 +19,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout "${{inputs.version_branch}}" branch
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
ref: "${{inputs.version_branch}}"
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin
cache: 'maven'
+ server-id: central
+ server-username: MAVEN_USERNAME
+ server-password: MAVEN_CENTRAL_TOKEN
+ gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
+ gpg-passphrase: MAVEN_GPG_PASSPHRASE
- name: Change version to release version
# Assume that RELEASE_VERSION will have form like: "v1.0.1". So we cut the "v"
@@ -37,15 +42,12 @@ jobs:
env:
RELEASE_VERSION: ${{ github.event.release.tag_name }}
- - name: Release Maven package
- uses: samuelmeuli/action-maven-publish@v1
- with:
- maven_profiles: "release"
- maven_args: ${{ env.MAVEN_ARGS }}
- gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
- gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }}
- nexus_username: ${{ secrets.OSSRH_USERNAME }}
- nexus_password: ${{ secrets.OSSRH_TOKEN }}
+ - name: Publish to Apache Maven Central
+ run: mvn package deploy -Prelease
+ env:
+ MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }}
+ MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }}
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
# This is separate job because there were issues with git after release step, was not able to commit changes.
update-working-version:
@@ -54,12 +56,12 @@ jobs:
if: "!contains(github.event.release.tag_name, 'RC')" # not sure we should keep this the RC part
steps:
- name: Checkout "${{inputs.version_branch}}" branch
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
ref: "${{inputs.version_branch}}"
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin
diff --git a/.github/workflows/snapshot-releases.yml b/.github/workflows/snapshot-releases.yml
index 66fe9d25a3..a12d9aaed5 100644
--- a/.github/workflows/snapshot-releases.yml
+++ b/.github/workflows/snapshot-releases.yml
@@ -16,9 +16,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
@@ -29,18 +29,22 @@ jobs:
runs-on: ubuntu-latest
needs: test
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
- distribution: temurin
java-version: 17
+ distribution: temurin
cache: 'maven'
- - name: Release Maven package
- uses: samuelmeuli/action-maven-publish@v1
- with:
- maven_profiles: "release"
- gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
- gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }}
- nexus_username: ${{ secrets.OSSRH_USERNAME }}
- nexus_password: ${{ secrets.OSSRH_TOKEN }}
+ server-id: central
+ server-username: MAVEN_USERNAME
+ server-password: MAVEN_CENTRAL_TOKEN
+ gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
+ gpg-passphrase: MAVEN_GPG_PASSPHRASE
+
+ - name: Publish to Apache Maven Central
+ run: mvn package deploy -Prelease
+ env:
+ MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }}
+ MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }}
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index b7a96edef6..370f36303c 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -23,9 +23,9 @@ jobs:
runs-on: ubuntu-latest
if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'java-operator-sdk' ) }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Java and Maven
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
diff --git a/README.md b/README.md
index 86edf232b0..72eb2f7974 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,10 @@ conversion hooks and dynamic admission controllers are supported as a separate p
Under the hood it uses the excellent [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client),
which provides additional facilities, like generating CRD from source code (and vice versa).
+
+
+Java Operator SDK is a CNCF project as part of [Operator Framework](https://github.com/operator-framework).
+
## Documentation
Documentation can be found on the **[JOSDK WebSite](https://javaoperatorsdk.io/)**.
@@ -63,6 +67,7 @@ projects want to advertise that fact here. For this reason, we ask that if you'd
to be featured in this section, please open a PR, adding a link to and short description of your
project, as shown below:
+- [kroxylicious](https://github.com/kroxylicious/kroxylicious/tree/main/kroxylicious-operator) Kafka proxy operator
- [ExposedApp operator](https://github.com/halkyonio/exposedapp-rhdblog): a sample operator
written to illustrate JOSDK concepts and its Quarkus extension in the ["Write Kubernetes
Operators in Java with the Java Operator SDK" blog series](https://developers.redhat.com/articles/2022/02/15/write-kubernetes-java-java-operator-sdk#).
diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml
index 44b50a3d81..3e292e810b 100644
--- a/bootstrapper-maven-plugin/pom.xml
+++ b/bootstrapper-maven-plugin/pom.xml
@@ -5,7 +5,7 @@
io.javaoperatorsdkjava-operator-sdk
- 5.0.5-SNAPSHOT
+ 5.1.3-SNAPSHOTbootstrapper
@@ -15,7 +15,7 @@
3.15.1
- 3.9.9
+ 3.9.113.0.03.15.1
@@ -58,7 +58,7 @@
commons-iocommons-io
- 2.19.0
+ 2.20.0com.github.spullara.mustache.java
diff --git a/bootstrapper-maven-plugin/src/main/resources/templates/ConfigMapDependentResource.java b/bootstrapper-maven-plugin/src/main/resources/templates/ConfigMapDependentResource.java
index a8d43c60db..59eae8b01c 100644
--- a/bootstrapper-maven-plugin/src/main/resources/templates/ConfigMapDependentResource.java
+++ b/bootstrapper-maven-plugin/src/main/resources/templates/ConfigMapDependentResource.java
@@ -17,10 +17,6 @@ public class ConfigMapDependentResource
public static final String KEY = "key";
- public ConfigMapDependentResource() {
- super(ConfigMap.class);
- }
-
@Override
protected ConfigMap desired({{artifactClassId}}CustomResource primary,
Context<{{artifactClassId}}CustomResource> context) {
diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml
index 924164c6cb..14e19dd85e 100644
--- a/caffeine-bounded-cache-support/pom.xml
+++ b/caffeine-bounded-cache-support/pom.xml
@@ -4,7 +4,7 @@
io.javaoperatorsdkjava-operator-sdk
- 5.0.5-SNAPSHOT
+ 5.1.3-SNAPSHOTcaffeine-bounded-cache-support
diff --git a/docs/content/en/blog/news/primary-cache-for-next-recon.md b/docs/content/en/blog/news/primary-cache-for-next-recon.md
new file mode 100644
index 0000000000..67326a6f17
--- /dev/null
+++ b/docs/content/en/blog/news/primary-cache-for-next-recon.md
@@ -0,0 +1,92 @@
+---
+title: How to guarantee allocated values for next reconciliation
+date: 2025-05-22
+author: >-
+ [Attila Mészáros](https://github.com/csviri) and [Chris Laprun](https://github.com/metacosm)
+---
+
+We recently released v5.1 of Java Operator SDK (JOSDK). One of the highlights of this release is related to a topic of
+so-called
+[allocated values](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#representing-allocated-values
+).
+
+To describe the problem, let's say that our controller needs to create a resource that has a generated identifier, i.e.
+a resource which identifier cannot be directly derived from the custom resource's desired state as specified in its
+`spec` field. To record the fact that the resource was successfully created, and to avoid attempting to
+recreate the resource again in subsequent reconciliations, it is typical for this type of controller to store the
+generated identifier in the custom resource's `status` field.
+
+The Java Operator SDK relies on the informers' cache to retrieve resources. These caches, however, are only guaranteed
+to be eventually consistent. It could happen that, if some other event occurs, that would result in a new
+reconciliation, **before** the update that's been made to our resource status has the chance to be propagated first to
+the cluster and then back to the informer cache, that the resource in the informer cache does **not** contain the latest
+version as modified by the reconciler. This would result in a new reconciliation where the generated identifier would be
+missing from the resource status and, therefore, another attempt to create the resource by the reconciler, which is not
+what we'd like.
+
+Java Operator SDK now provides a utility class [
+`PrimaryUpdateAndCacheUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java)
+to handle this particular use case. Using that overlay cache, your reconciler is guaranteed to see the most up-to-date
+version of the resource on the next reconciliation:
+
+```java
+
+@Override
+public UpdateControl reconcile(
+ StatusPatchCacheCustomResource resource,
+ Context context) {
+
+ // omitted code
+
+ var freshCopy = createFreshCopy(resource); // need fresh copy just because we use the SSA version of update
+ freshCopy
+ .getStatus()
+ .setValue(statusWithAllocatedValue());
+
+ // using the utility instead of update control to patch the resource status
+ var updated =
+ PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource(resource, freshCopy, context);
+ return UpdateControl.noUpdate();
+}
+```
+
+How does `PrimaryUpdateAndCacheUtils` work?
+There are multiple ways to solve this problem, but ultimately, we only provide the solution described below. If you
+want to dig deep in alternatives, see
+this [PR](https://github.com/operator-framework/java-operator-sdk/pull/2800/files).
+
+The trick is to intercept the resource that the reconciler updated and cache that version in an additional cache on top
+of the informer's cache. Subsequently, if the reconciler needs to read the resource, the SDK will first check if it is
+in the overlay cache and read it from there if present, otherwise read it from the informer's cache. If the informer
+receives an event with a fresh resource, we always remove the resource from the overlay cache, since that is a more
+recent resource. But this **works only** if the reconciler updates the resource using **optimistic locking**.
+If the update fails on conflict, because the resource has already been updated on the cluster before we got
+the chance to get our update in, we simply wait and poll the informer cache until the new resource version from the
+server appears in the informer's cache,
+and then try to apply our updates to the resource again using the updated version from the server, again with optimistic
+locking.
+
+So why is optimistic locking required? We hinted at it above, but the gist of it, is that if another party updates the
+resource before we get a chance to, we wouldn't be able to properly handle the resulting situation correctly in all
+cases. The informer would receive that new event before our own update would get a chance to propagate. Without
+optimistic locking, there wouldn't be a fail-proof way to determine which update should prevail (i.e. which occurred
+first), in particular in the event of the informer losing the connection to the cluster or other edge cases (the joys of
+distributed computing!).
+
+Optimistic locking simplifies the situation and provides us with stronger guarantees: if the update succeeds, then we
+can be sure we have the proper resource version in our caches. The next event will contain our update in all cases.
+Because we know that, we can also be sure that we can evict the cached resource in the overlay cache whenever we receive
+a new event. The overlay cache is only used if the SDK detects that the original resource (i.e. the one before we
+applied our status update in the example above) is still in the informer's cache.
+
+The following diagram sums up the process:
+
+```mermaid
+flowchart TD
+ A["Update Resource with Lock"] --> B{"Is Successful"}
+ B -- Fails on conflict --> D["Poll the Informer cache until resource updated"]
+ D --> A
+ B -- Yes --> n2{"Original resource still in informer cache?"}
+ n2 -- Yes --> C["Cache the resource in overlay cache"]
+ n2 -- No --> n3["Informer cache already contains up-to-date version, do not use overlay cache"]
+```
diff --git a/docs/content/en/docs/documentation/configuration.md b/docs/content/en/docs/documentation/configuration.md
index 052c0e0f19..06eda5f2a2 100644
--- a/docs/content/en/docs/documentation/configuration.md
+++ b/docs/content/en/docs/documentation/configuration.md
@@ -113,14 +113,53 @@ for this feature.
## DependentResource-level configuration
-`DependentResource` implementations can implement the `DependentResourceConfigurator` interface
-to pass information to the implementation. For example, the SDK
-provides specific support for the `KubernetesDependentResource`, which can be configured via the
-`@KubernetesDependent` annotation. This annotation is, in turn, converted into a
-`KubernetesDependentResourceConfig` instance, which is then passed to the `configureWith` method
-implementation.
+It is possible to define custom annotations to configure custom `DependentResource` implementations. In order to provide
+such a configuration mechanism for your own `DependentResource` implementations, they must be annotated with the
+`@Configured` annotation. This annotation defines 3 fields that tie everything together:
+
+- `by`, which specifies which annotation class will be used to configure your dependents,
+- `with`, which specifies the class holding the configuration object for your dependents and
+- `converter`, which specifies the `ConfigurationConverter` implementation in charge of converting the annotation
+ specified by the `by` field into objects of the class specified by the `with` field.
+
+`ConfigurationConverter` instances implement a single `configFrom` method, which will receive, as expected, the
+annotation instance annotating the dependent resource instance to be configured, but it can also extract information
+from the `DependentResourceSpec` instance associated with the `DependentResource` class so that metadata from it can be
+used in the configuration, as well as the parent `ControllerConfiguration`, if needed. The role of
+`ConfigurationConverter` implementations is to extract the annotation information, augment it with metadata from the
+`DependentResourceSpec` and the configuration from the parent controller on which the dependent is defined, to finally
+create the configuration object that the `DependentResource` instances will use.
+
+However, one last element is required to finish the configuration process: the target `DependentResource` class must
+implement the `ConfiguredDependentResource` interface, parameterized with the annotation class defined by the
+`@Configured` annotation `by` field. This interface is called by the framework to inject the configuration at the
+appropriate time and retrieve the configuration, if it's available.
+
+For example, `KubernetesDependentResource`, a core implementation that the framework provides, can be configured via the
+`@KubernetesDependent` annotation. This set up is configured as follows:
-TODO
+```java
+
+@Configured(
+ by = KubernetesDependent.class,
+ with = KubernetesDependentResourceConfig.class,
+ converter = KubernetesDependentConverter.class)
+public abstract class KubernetesDependentResource
+ extends AbstractEventSourceHolderDependentResource>
+ implements ConfiguredDependentResource> {
+ // code omitted
+}
+```
+
+The `@Configured` annotation specifies that `KubernetesDependentResource` instances can be configured by using the
+`@KubernetesDependent` annotation, which gets converted into a `KubernetesDependentResourceConfig` object by a
+`KubernetesDependentConverter`. That configuration object is then injected by the framework in the
+`KubernetesDependentResource` instance, after it's been created, because the class implements the
+`ConfiguredDependentResource` interface, properly parameterized.
+
+For more information on how to use this feature, we recommend looking at how this mechanism is implemented for
+`KubernetesDependentResource` in the core framework, `SchemaDependentResource` in the samples or `CustomAnnotationDep`
+in the `BaseConfigurationServiceTest` test class.
## EventSource-level configuration
diff --git a/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md b/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md
index b9fcb7acf5..7416949869 100644
--- a/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md
+++ b/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md
@@ -133,13 +133,9 @@ Deleted (or set to be garbage collected). The following example shows how to cre
```java
-@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR)
+@KubernetesDependent(informer = @Informer(labelSelector = SELECTOR))
class DeploymentDependentResource extends CRUDKubernetesDependentResource {
- public DeploymentDependentResource() {
- super(Deployment.class);
- }
-
@Override
protected Deployment desired(WebPage webPage, Context context) {
var deploymentName = deploymentName(webPage);
@@ -178,9 +174,10 @@ JOSDK will take the appropriate steps to wire everything together and call your
`DependentResource` implementations `reconcile` method before your primary resource is reconciled.
This makes sense in most use cases where the logic associated with the primary resource is
usually limited to status handling based on the state of the secondary resources and the
-resources are not dependent on each other.
+resources are not dependent on each other. As an alternative, you can also invoke reconciliation explicitly,
+event for managed workflows.
-See [Workflows](https://javaoperatorsdk.io/docs/workflows) for more details on how the dependent
+See [Workflows](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/workflows/) for more details on how the dependent
resources are reconciled.
This behavior and automated handling is referred to as "managed" because the `DependentResource`
@@ -188,12 +185,14 @@ instances are managed by JOSDK, an example of which can be seen below:
```java
-@ControllerConfiguration(
- labelSelector = SELECTOR,
+@Workflow(
dependents = {
@Dependent(type = ConfigMapDependentResource.class),
@Dependent(type = DeploymentDependentResource.class),
- @Dependent(type = ServiceDependentResource.class)
+ @Dependent(type = ServiceDependentResource.class),
+ @Dependent(
+ type = IngressDependentResource.class,
+ reconcilePrecondition = ExposedIngressCondition.class)
})
public class WebPageManagedDependentsReconciler
implements Reconciler, ErrorStatusHandler {
@@ -208,7 +207,6 @@ public class WebPageManagedDependentsReconciler
webPage.setStatus(createStatus(name));
return UpdateControl.patchStatus(webPage);
}
-
}
```
@@ -222,7 +220,7 @@ It is also possible to wire dependent resources programmatically. In practice th
developer is responsible for initializing and managing the dependent resources as well as calling
their `reconcile` method. However, this makes it possible for developers to fully customize the
reconciliation process. Standalone dependent resources should be used in cases when the managed use
-case does not fit. You can, of course, also use [Workflows](https://javaoperatorsdk.io/docs/workflows) when managing
+case does not fit. You can, of course, also use [Workflows](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/workflows/) when managing
resources programmatically.
You can see a commented example of how to do
diff --git a/docs/content/en/docs/documentation/dependent-resource-and-workflows/workflows.md b/docs/content/en/docs/documentation/dependent-resource-and-workflows/workflows.md
index 4b1bea6790..c5ee83a446 100644
--- a/docs/content/en/docs/documentation/dependent-resource-and-workflows/workflows.md
+++ b/docs/content/en/docs/documentation/dependent-resource-and-workflows/workflows.md
@@ -12,12 +12,12 @@ depends on the state of other resources or cannot be processed until these other
a given state or some condition holds true for them. Dealing with such scenarios are therefore
rather common for operators and the purpose of the workflow feature of the Java Operator SDK
(JOSDK) is to simplify supporting such cases in a declarative way. Workflows build on top of the
-[dependent resources](https://javaoperatorsdk.io/docs/dependent-resources) feature.
+[dependent resources](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/) feature.
While dependent resources focus on how a given secondary resource should be reconciled,
workflows focus on orchestrating how these dependent resources should be reconciled.
Workflows describe how as a set of
-[dependent resources](https://javaoperatorsdk.io/docs/dependent-resources) (DR) depend on one
+[dependent resources](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/) (DR) depend on one
another, along with the conditions that need to hold true at certain stages of the
reconciliation process.
@@ -135,7 +135,7 @@ public class SampleWorkflowReconciler implements Reconciler reconcile(
StatusPatchCacheCustomResource resource, Context context) {
@@ -201,85 +201,22 @@ public UpdateControl reconcile(
var freshCopy = createFreshCopy(primary);
freshCopy.getStatus().setValue(statusWithState());
- var updatedResource = PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatus(resource, freshCopy, context);
-
- return UpdateControl.noUpdate();
- }
-```
-
-In the background `PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatus` puts the result of the update into an internal
-cache and will make sure that the next reconciliation will contain the most recent version of the resource. Note that it
-is not necessarily the version of the resource you got as response from the update, it can be newer since other parties
-can do additional updates meanwhile, but if not explicitly modified, it will contain the up-to-date status.
-
-See related integration test [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/internal).
-
-This approach works with the default configuration of the framework and should be good to go in most of the cases.
-Without going further into the details, this won't work if `ConfigurationService.parseResourceVersionsForEventFilteringAndCaching`
-is set to `false` (more precisely there are some edge cases when it won't work). For that case framework provides the following solution:
-
-#### Fallback approach: using `PrimaryResourceCache` cache
-
-As an alternative, for very rare cases when `ConfigurationService.parseResourceVersionsForEventFilteringAndCaching`
-needs to be set to `false` you can use an explicit caching approach:
-
-```java
-
-// We on purpose don't use the provided predicate to show what a custom one could look like.
- private final PrimaryResourceCache cache =
- new PrimaryResourceCache<>(
- (statusPatchCacheCustomResourcePair, statusPatchCacheCustomResource) ->
- statusPatchCacheCustomResource.getStatus().getValue()
- >= statusPatchCacheCustomResourcePair.afterUpdate().getStatus().getValue());
-
- @Override
- public UpdateControl reconcile(
- StatusPatchPrimaryCacheCustomResource primary,
- Context context) {
-
- // cache will compare the current and the cached resource and return the more recent. (And evict the old)
- primary = cache.getFreshResource(primary);
-
- // omitted logic
-
- var freshCopy = createFreshCopy(primary);
-
- freshCopy.getStatus().setValue(statusWithState());
+ var updatedResource = PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource(resource, freshCopy, context);
- var updated =
- PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatus(primary, freshCopy, context, cache);
-
+ // the resource was updated transparently via the utils, no further action is required via UpdateControl in this case
return UpdateControl.noUpdate();
}
-
- @Override
- public DeleteControl cleanup(
- StatusPatchPrimaryCacheCustomResource resource,
- Context context)
- throws Exception {
- // cleanup the cache on resource deletion
- cache.cleanup(resource);
- return DeleteControl.defaultDelete();
- }
-
```
-[`PrimaryResourceCache`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/support/PrimaryResourceCache.java)
-is designed for this purpose. As shown in the example above, it is up to you to provide a predicate to determine if the
-resource is more recent than the one available. In other words, when to evict the resource from the cache. Typically, as
-shown in
-the [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/primarycache)
-you can have a counter in status to check on that.
-
-Since all of this happens explicitly, you cannot use this approach for managed dependent resources and workflows and
-will need to use the unmanaged approach instead. This is due to the fact that managed dependent resources always get
-their associated primary resource from the underlying informer event source cache.
-
-#### Additional remarks
+After the update `PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource` puts the result of the update into an internal
+cache and the framework will make sure that the next reconciliation contains the most recent version of the resource.
+Note that it is not necessarily the same version returned as response from the update, it can be a newer version since other parties
+can do additional updates meanwhile. However, unless it has been explicitly modified, that
+resource will contain the up-to-date status.
-As shown in the integration tests, there is no optimistic locking used when updating the
-[resource](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/internal/StatusPatchCacheReconciler.java#L41)
-(in other words `metadata.resourceVersion` is set to `null`). This is desired since you don't want the patch to fail on
-update.
+Note that you can also perform additional updates after the `PrimaryUpdateAndCacheUtils.*PatchStatusAndCacheResource` is
+called, either by calling any of the `PrimeUpdateAndCacheUtils` methods again or via `UpdateControl`. Using
+`PrimaryUpdateAndCacheUtils` guarantees that the next reconciliation will see a resource state no older than the version
+updated via `PrimaryUpdateAndCacheUtils`.
-In addition, you can configure the [Fabric8 client retry](https://github.com/fabric8io/kubernetes-client?tab=readme-ov-file#configuring-the-client).
+See related integration test [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache).
diff --git a/docs/content/en/docs/faq/_index.md b/docs/content/en/docs/faq/_index.md
index 9308ce4cfa..1c3d82fe35 100644
--- a/docs/content/en/docs/faq/_index.md
+++ b/docs/content/en/docs/faq/_index.md
@@ -90,8 +90,38 @@ reconciliation. For example if you patch the status at the end of the reconcilia
it is not guaranteed that during the next reconciliation you will see the fresh resource. Therefore, controllers
which do this, usually cache the updated status in memory to make sure it is present for next reconciliation.
-Dependent Resources feature supports the [first approach](../dependent-resources/_index.md#external-state-tracking-dependent-resources).
+From version 5.1 you can use [this utility](../documentation/reconciler.md#making-sure-the-primary-resource-is-up-to-date-for-the-next-reconciliation)
+to make sure an updated status is present for the next reconciliation.
+
+Dependent Resources feature supports the [first approach](../documentation/dependent-resource-and-workflows/dependent-resources.md#external-state-tracking-dependent-resources).
+### How can I make the status update of my custom resource trigger a reconciliation?
+
+The framework checks, by default, when an event occurs, that could trigger a reconciliation, if the event increased the
+`generation` field of the primary resource's metadata and filters out the event if it did not. `generation` is typically
+only increased when the `.spec` field of a resource is changed. As a result, a change in the `.status` field would not
+normally trigger a reconciliation.
+
+To change this behavior, you can set the [
+`generationAwareEventProcessing`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java#L43)
+to `false`:
+
+```java
+
+@ControllerConfiguration(generationAwareEventProcessing = false)
+static class TestCustomReconciler implements Reconciler {
+
+ @Override
+ public UpdateControl reconcile(TestCustomResource resource, Context context) {
+ // code omitted
+ }
+}
+```
+
+For secondary resources, every change should trigger a reconciliation by default, except when you add explicit filters
+or use dependent resource implementations that filter out changes they trigger themselves by default,
+see [related docs](../documentation/dependent-resource-and-workflows/dependent-resources.md#caching-and-event-handling-in-kubernetesdependentresource).
+
### How can I skip the reconciliation of a dependent resource?
Skipping workflow reconciliation altogether is possible with the explicit invocation feature since v5.
diff --git a/docs/content/en/docs/migration/v3-1-migration.md b/docs/content/en/docs/migration/v3-1-migration.md
index e42c4a206a..b4b42d9a5e 100644
--- a/docs/content/en/docs/migration/v3-1-migration.md
+++ b/docs/content/en/docs/migration/v3-1-migration.md
@@ -13,7 +13,7 @@ renamed accordingly.
Version 3.1 comes with a workflow engine that replaces the previous behavior of managed dependent
resources.
-See [Workflows documentation](https://javaoperatorsdk.io/docs/workflows) for further details.
+See [Workflows documentation](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/workflows/) for further details.
The primary impact after upgrade is a change of the order in which managed dependent resources
are reconciled. They are now reconciled in parallel with optional ordering defined using the
['depends_on'](https://github.com/java-operator-sdk/java-operator-sdk/blob/df44917ef81725c10bbcb772ab7b434d511b13b9/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java#L23-L23)
diff --git a/docs/content/en/docs/migration/v4-4-migration.md b/docs/content/en/docs/migration/v4-4-migration.md
index 998e6ddf9a..913c08b843 100644
--- a/docs/content/en/docs/migration/v4-4-migration.md
+++ b/docs/content/en/docs/migration/v4-4-migration.md
@@ -55,7 +55,7 @@ explicitly to the Operator constructor, it is now recommended to provide that va
## Using Server-Side Apply in Dependent Resources
From this version by
-default [Dependent Resources](https://javaoperatorsdk.io/docs/dependent-resources) use
+default [Dependent Resources](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/) use
[Server Side Apply (SSA)](https://kubernetes.io/docs/reference/using-api/server-side-apply/) to
create and
update Kubernetes resources. A
diff --git a/docs/content/en/docs/migration/v4-5-migration.md b/docs/content/en/docs/migration/v4-5-migration.md
index eff1581d87..42e78d76dc 100644
--- a/docs/content/en/docs/migration/v4-5-migration.md
+++ b/docs/content/en/docs/migration/v4-5-migration.md
@@ -5,7 +5,7 @@ permalink: /docs/v4-5-migration
---
Version 4.5 introduces improvements related to event handling for Dependent Resources, more precisely the
-[caching and event handling](https://javaoperatorsdk.io/docs/dependent-resources#caching-and-event-handling-in-kubernetesdependentresource)
+[caching and event handling](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/#caching-and-event-handling-in-kubernetesdependentresource)
features. As a result the Kubernetes resources managed using
[KubernetesDependentResource](https://github.com/java-operator-sdk/java-operator-sdk/blob/73b1d8db926a24502c3a70da34f6bcac4f66b4eb/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java#L72-L72)
or its subclasses, will add an annotation recording the resource's version whenever JOSDK updates or creates such
diff --git a/docs/static/images/cncf_logo2.png b/docs/static/images/cncf_logo2.png
new file mode 100644
index 0000000000..e1236b7e87
Binary files /dev/null and b/docs/static/images/cncf_logo2.png differ
diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml
index 3c568e76fd..ea18d07ce7 100644
--- a/micrometer-support/pom.xml
+++ b/micrometer-support/pom.xml
@@ -4,7 +4,7 @@
io.javaoperatorsdkjava-operator-sdk
- 5.0.5-SNAPSHOT
+ 5.1.3-SNAPSHOTmicrometer-support
diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml
index e1cff7980d..5809eed20f 100644
--- a/operator-framework-bom/pom.xml
+++ b/operator-framework-bom/pom.xml
@@ -4,7 +4,7 @@
io.javaoperatorsdkoperator-framework-bom
- 5.0.5-SNAPSHOT
+ 5.1.3-SNAPSHOTpomOperator SDK - Bill of MaterialsJava SDK for implementing Kubernetes operators
@@ -33,19 +33,12 @@
https://github.com/operator-framework/java-operator-sdk/tree/master
-
-
- ossrh
- https://oss.sonatype.org/content/repositories/snapshots
-
-
-
- 1.7.0
- 3.2.7
+ 3.2.83.3.1
- 3.11.2
+ 3.11.32.44.3
+ 0.8.0
@@ -115,6 +108,17 @@
release
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ **/*IT.java
+ **/*E2E.java
+ **/InformerRelatedBehaviorTest.java
+
+
+ org.apache.maven.pluginsmaven-javadoc-plugin
@@ -138,13 +142,13 @@
jar
+ verifyorg.apache.maven.pluginsmaven-gpg-plugin
- ${maven-gpg-plugin.version}sign-artifacts
@@ -162,14 +166,15 @@
- org.sonatype.plugins
- nexus-staging-maven-plugin
- ${nexus-staging-maven-plugin.version}
+ org.sonatype.central
+ central-publishing-maven-plugin
+ ${central-publishing-maven-plugin.version}true
- ossrh
- https://oss.sonatype.org/
- true
+ central
+ true
+ true
+ published
diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml
index cad50ebc32..c99b609113 100644
--- a/operator-framework-core/pom.xml
+++ b/operator-framework-core/pom.xml
@@ -4,7 +4,7 @@
io.javaoperatorsdkjava-operator-sdk
- 5.0.5-SNAPSHOT
+ 5.1.3-SNAPSHOT../pom.xml
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java
index b7fbce7f07..0495131d79 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java
@@ -5,6 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.health.EventSourceHealthIndicator;
import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator;
import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource;
@@ -22,7 +23,7 @@ public class RuntimeInfo {
private final Operator operator;
public RuntimeInfo(Operator operator) {
- this.registeredControllers = operator.getRegisteredControllers();
+ this.registeredControllers = Collections.unmodifiableSet(operator.getRegisteredControllers());
this.operator = operator;
}
@@ -30,6 +31,7 @@ public boolean isStarted() {
return operator.isStarted();
}
+ @SuppressWarnings("unused")
public Set getRegisteredControllers() {
checkIfStarted();
return registeredControllers;
@@ -80,4 +82,23 @@ public Map> unhealthyEventSource
}
return res;
}
+
+ /**
+ * Retrieves the {@link RegisteredController} associated with the specified controller name or
+ * {@code null} if no such controller is registered.
+ *
+ * @param controllerName the name of the {@link RegisteredController} to retrieve
+ * @return the {@link RegisteredController} associated with the specified controller name or
+ * {@code null} if no such controller is registered
+ * @since 5.1.2
+ */
+ @SuppressWarnings({"unchecked", "unused"})
+ public RegisteredController extends HasMetadata> getRegisteredController(
+ String controllerName) {
+ checkIfStarted();
+ return registeredControllers.stream()
+ .filter(rc -> rc.getConfiguration().getName().equals(controllerName))
+ .findFirst()
+ .orElse(null);
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
index 438f7d91a9..891f199dbe 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
@@ -30,6 +30,12 @@
import static io.javaoperatorsdk.operator.api.config.ControllerConfiguration.CONTROLLER_NAME_AS_FIELD_MANAGER;
+/**
+ * A default {@link ConfigurationService} implementation, resolving {@link Reconciler}s
+ * configuration when it has already been resolved before. If this behavior is not adequate, please
+ * use {@link AbstractConfigurationService} instead as a base for your {@code ConfigurationService}
+ * implementation.
+ */
public class BaseConfigurationService extends AbstractConfigurationService {
private static final String LOGGER_NAME = "Default ConfigurationService implementation";
@@ -149,10 +155,12 @@ private static void configureFromAnnotatedReconciler(
@Override
protected void logMissingReconcilerWarning(String reconcilerKey, String reconcilersNameMessage) {
- logger.warn(
- "Configuration for reconciler '{}' was not found. {}",
- reconcilerKey,
- reconcilersNameMessage);
+ if (!createIfNeeded()) {
+ logger.warn(
+ "Configuration for reconciler '{}' was not found. {}",
+ reconcilerKey,
+ reconcilersNameMessage);
+ }
}
@SuppressWarnings("unused")
@@ -318,6 +326,13 @@ private
ResolvedControllerConfiguration
controllerCon
informerConfig);
}
+ /**
+ * @deprecated This method was meant to allow subclasses to prevent automatic creation of the
+ * configuration when not found. This functionality is now removed, if you want to be able to
+ * prevent automated, on-demand creation of a reconciler's configuration, please use the
+ * {@link AbstractConfigurationService} implementation instead as base for your extension.
+ */
+ @Deprecated(forRemoval = true)
protected boolean createIfNeeded() {
return true;
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
index 3ffc913c5e..41134e64ac 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
@@ -13,6 +13,8 @@
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.CustomResource;
@@ -26,7 +28,6 @@
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
-import io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceUpdaterMatcher;
import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory;
import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource;
@@ -394,9 +395,6 @@ default boolean shouldUseSSA(
Class extends KubernetesDependentResource> dependentResourceType,
Class extends HasMetadata> resourceType,
KubernetesDependentResourceConfig extends HasMetadata> config) {
- if (ResourceUpdaterMatcher.class.isAssignableFrom(dependentResourceType)) {
- return false;
- }
Boolean useSSAConfig =
Optional.ofNullable(config).map(KubernetesDependentResourceConfig::useSSA).orElse(null);
// don't use SSA for certain resources by default, only if explicitly overridden
@@ -442,12 +440,40 @@ default Set> defaultNonSSAResource() {
*
* @return if special annotation should be used for dependent resource to filter events
* @since 4.5.0
- * @return if special annotation should be used for dependent resource to filter events
*/
default boolean previousAnnotationForDependentResourcesEventFiltering() {
return true;
}
+ /**
+ * For dependent resources, the framework can add an annotation to filter out events resulting
+ * directly from the framework's operation. There are, however, some resources that do not follow
+ * the Kubernetes API conventions that changes in metadata should not increase the generation of
+ * the resource (as recorded in the {@code generation} field of the resource's {@code metadata}).
+ * For these resources, this convention is not respected and results in a new event for the
+ * framework to process. If that particular case is not handled correctly in the resource matcher,
+ * the framework will consider that the resource doesn't match the desired state and therefore
+ * triggers an update, which in turn, will re-add the annotation, thus starting the loop again,
+ * infinitely.
+ *
+ *
As a workaround, we automatically skip adding previous annotation for those well-known
+ * resources. Note that if you are sure that the matcher works for your use case, and it should in
+ * most instances, you can remove the resource type from the blocklist.
+ *
+ *
The consequence of adding a resource type to the set is that the framework will not use
+ * event filtering to prevent events, initiated by changes made by the framework itself as a
+ * result of its processing of dependent resources, to trigger the associated reconciler again.
+ *
+ *
Note that this method only takes effect if annotating dependent resources to prevent
+ * dependent resources events from triggering the associated reconciler again is activated as
+ * controlled by {@link #previousAnnotationForDependentResourcesEventFiltering()}
+ *
+ * @return a Set of resource classes where the previous version annotation won't be used.
+ */
+ default Set> withPreviousAnnotationForDependentResourcesBlocklist() {
+ return Set.of(Deployment.class, StatefulSet.class);
+ }
+
/**
* If the event logic should parse the resourceVersion to determine the ordering of dependent
* resource events. This is typically not needed.
@@ -459,7 +485,6 @@ default boolean previousAnnotationForDependentResourcesEventFiltering() {
*
* @return if resource version should be parsed (as integer)
* @since 4.5.0
- * @return if resource version should be parsed (as integer)
*/
default boolean parseResourceVersionsForEventFilteringAndCaching() {
return false;
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
index f420be0fff..be86cbe312 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
@@ -40,6 +40,7 @@ public class ConfigurationServiceOverrider {
private Boolean parseResourceVersions;
private Boolean useSSAToPatchPrimaryResource;
private Boolean cloneSecondaryResourcesWhenGettingFromCache;
+ private Set> previousAnnotationUsageBlocklist;
@SuppressWarnings("rawtypes")
private DependentResourceFactory dependentResourceFactory;
@@ -188,6 +189,12 @@ public ConfigurationServiceOverrider withCloneSecondaryResourcesWhenGettingFromC
return this;
}
+ public ConfigurationServiceOverrider withPreviousAnnotationForDependentResourcesBlocklist(
+ Set> blocklist) {
+ this.previousAnnotationUsageBlocklist = blocklist;
+ return this;
+ }
+
public ConfigurationService build() {
return new BaseConfigurationService(original.getVersion(), cloner, client) {
@Override
@@ -247,13 +254,20 @@ public boolean closeClientOnStop() {
@Override
public ExecutorService getExecutorService() {
- return overriddenValueOrDefault(executorService, ConfigurationService::getExecutorService);
+ if (executorService != null) {
+ return executorService;
+ } else {
+ return super.getExecutorService();
+ }
}
@Override
public ExecutorService getWorkflowExecutorService() {
- return overriddenValueOrDefault(
- workflowExecutorService, ConfigurationService::getWorkflowExecutorService);
+ if (workflowExecutorService != null) {
+ return workflowExecutorService;
+ } else {
+ return super.getWorkflowExecutorService();
+ }
}
@Override
@@ -328,6 +342,14 @@ public boolean cloneSecondaryResourcesWhenGettingFromCache() {
cloneSecondaryResourcesWhenGettingFromCache,
ConfigurationService::cloneSecondaryResourcesWhenGettingFromCache);
}
+
+ @Override
+ public Set>
+ withPreviousAnnotationForDependentResourcesBlocklist() {
+ return overriddenValueOrDefault(
+ previousAnnotationUsageBlocklist,
+ ConfigurationService::withPreviousAnnotationForDependentResourcesBlocklist);
+ }
};
}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java
index f11fc47eef..3b6f94a025 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java
@@ -134,6 +134,39 @@ public static Class> getTypeArgumentFromExtendedClassByIndex(Class> clazz, i
}
}
+ public static Class> getTypeArgumentFromHierarchyByIndex(Class> clazz, int index) {
+ return getTypeArgumentFromHierarchyByIndex(clazz, null, index);
+ }
+
+ public static Class> getTypeArgumentFromHierarchyByIndex(
+ Class> clazz, Class> expectedImplementedInterface, int index) {
+ Class> c = clazz;
+ while (!(c.getGenericSuperclass() instanceof ParameterizedType)) {
+ c = c.getSuperclass();
+ }
+ Class> actualTypeArgument =
+ (Class>) ((ParameterizedType) c.getGenericSuperclass()).getActualTypeArguments()[index];
+ if (expectedImplementedInterface != null
+ && !expectedImplementedInterface.isAssignableFrom(actualTypeArgument)) {
+ throw new IllegalArgumentException(
+ GENERIC_PARAMETER_TYPE_ERROR_PREFIX
+ + clazz.getName()
+ + "because it doesn't extend a class that is parametrized with the type that"
+ + " implements "
+ + expectedImplementedInterface.getSimpleName()
+ + ". Please provide the resource type in the constructor (e.g.,"
+ + " super(Deployment.class).");
+ } else if (expectedImplementedInterface == null && actualTypeArgument.equals(Object.class)) {
+ throw new IllegalArgumentException(
+ GENERIC_PARAMETER_TYPE_ERROR_PREFIX
+ + clazz.getName()
+ + " because it doesn't extend a class that is parametrized with the type we want to"
+ + " retrieve or because it's Object.class. Please provide the resource type in the "
+ + "constructor (e.g., super(Deployment.class).");
+ }
+ return actualTypeArgument;
+ }
+
public static Class> getFirstTypeArgumentFromInterface(
Class> clazz, Class> expectedImplementedInterface) {
return getTypeArgumentFromInterfaceByIndex(clazz, expectedImplementedInterface, 0);
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java
deleted file mode 100644
index a0c9dc67ae..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package io.javaoperatorsdk.operator.api.config.dependent;
-
-public interface DependentResourceConfigurationProvider {
- @SuppressWarnings("rawtypes")
- Object getConfigurationFor(DependentResourceSpec spec);
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java
index 9fb5ad4c82..2369d5f523 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java
@@ -194,6 +194,14 @@ public Builder withNamespaces(Set namespaces) {
return this;
}
+ /**
+ * @since 5.1.1
+ */
+ public Builder withNamespaces(String... namespaces) {
+ config.withNamespaces(Set.of(namespaces));
+ return this;
+ }
+
public Builder withNamespacesInheritedFromController() {
withNamespaces(SAME_AS_CONTROLLER_NAMESPACES_SET);
return this;
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java
index 174f7667f6..c61cc837c1 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java
@@ -1,146 +1,89 @@
package io.javaoperatorsdk.operator.api.reconciler;
-import java.util.function.Supplier;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
import java.util.function.UnaryOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
import io.fabric8.kubernetes.client.dsl.base.PatchType;
-import io.javaoperatorsdk.operator.api.reconciler.support.PrimaryResourceCache;
+import io.javaoperatorsdk.operator.OperatorException;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
/**
* Utility methods to patch the primary resource state and store it to the related cache, to make
- * sure that fresh resource is present for the next reconciliation. The main use case for such
- * updates is to store state is resource status. Use of optimistic locking is not desired for such
- * updates, since we don't want to patch fail and lose information that we want to store.
+ * sure that the latest version of the resource is present for the next reconciliation. The main use
+ * case for such updates is to store state is resource status.
+ *
+ *
The way the framework handles this is with retryable updates with optimistic locking, and
+ * caches the updated resource from the response in an overlay cache on top of the Informer cache.
+ * If the update fails, it reads the primary resource from the cluster, applies the modifications
+ * again and retries the update.
*/
public class PrimaryUpdateAndCacheUtils {
+ public static final int DEFAULT_MAX_RETRY = 10;
+ public static final int DEFAULT_RESOURCE_CACHE_TIMEOUT_MILLIS = 10000;
+ public static final int DEFAULT_RESOURCE_CACHE_POLL_PERIOD_MILLIS = 50;
+
private PrimaryUpdateAndCacheUtils() {}
private static final Logger log = LoggerFactory.getLogger(PrimaryUpdateAndCacheUtils.class);
/**
- * Makes sure that the up-to-date primary resource will be present during the next reconciliation.
- * Using update (PUT) method.
- *
- * @param primary resource
- * @param context of reconciliation
- * @return updated resource
- * @param
primary resource type
- */
- public static
P updateAndCacheStatus(P primary, Context
context) {
- logWarnIfResourceVersionPresent(primary);
- return patchAndCacheStatus(
- primary, context, () -> context.getClient().resource(primary).updateStatus());
- }
-
- /**
- * Makes sure that the up-to-date primary resource will be present during the next reconciliation.
- * Using JSON Merge patch.
- *
- * @param primary resource
- * @param context of reconciliation
- * @return updated resource
- * @param
primary resource type
+ * Updates the status with optimistic locking and caches the result for next reconciliation. For
+ * details see {@link #updateAndCacheResource}.
*/
- public static
P updateStatusAndCacheResource(
+ P primary, Context
context, UnaryOperator
modificationFunction) {
+ return updateAndCacheResource(
+ primary,
+ context,
+ modificationFunction,
+ r -> context.getClient().resource(r).updateStatus());
}
/**
- * Makes sure that the up-to-date primary resource will be present during the next reconciliation.
- * Using JSON Patch.
- *
- * @param primary resource
- * @param context of reconciliation
- * @return updated resource
- * @param
primary resource type
+ * Patches the status using JSON Merge Patch with optimistic locking and caches the result for
+ * next reconciliation. For details see {@link #updateAndCacheResource}.
*/
- public static
P mergePatchStatusAndCacheResource(
+ P primary, Context
context, UnaryOperator
modificationFunction) {
+ return updateAndCacheResource(
+ primary, context, modificationFunction, r -> context.getClient().resource(r).patchStatus());
}
/**
- * Makes sure that the up-to-date primary resource will be present during the next reconciliation.
- *
- * @param primary resource
- * @param context of reconciliation
- * @param patch free implementation of cache
- * @return the updated resource.
- * @param
primary resource type
+ * Patches the status using JSON Patch with optimistic locking and caches the result for next
+ * reconciliation. For details see {@link #updateAndCacheResource}.
*/
- public static
P patchStatusAndCacheResource(
+ P primary, Context
context, UnaryOperator
modificationFunction) {
+ return updateAndCacheResource(
+ primary,
+ context,
+ UnaryOperator.identity(),
+ r -> context.getClient().resource(r).editStatus(modificationFunction));
}
/**
- * Makes sure that the up-to-date primary resource will be present during the next reconciliation.
- * Using Server Side Apply.
- *
- * @param primary resource
- * @param freshResourceWithStatus - fresh resource with target state
- * @param context of reconciliation
- * @return the updated resource.
- * @param
primary resource type
+ * Patches the status using Server Side Apply with optimistic locking and caches the result for
+ * next reconciliation. For details see {@link #updateAndCacheResource}.
*/
- public static
P ssaPatchAndCacheStatus(
+ public static
P ssaPatchStatusAndCacheResource(
P primary, P freshResourceWithStatus, Context
context) {
- logWarnIfResourceVersionPresent(freshResourceWithStatus);
- var res =
- context
- .getClient()
- .resource(freshResourceWithStatus)
- .subresource("status")
- .patch(
- new PatchContext.Builder()
- .withForce(true)
- .withFieldManager(context.getControllerConfiguration().fieldManager())
- .withPatchType(PatchType.SERVER_SIDE_APPLY)
- .build());
-
- context
- .eventSourceRetriever()
- .getControllerEventSource()
- .handleRecentResourceUpdate(ResourceID.fromResource(primary), res, primary);
- return res;
- }
-
- /**
- * Patches the resource and adds it to the {@link PrimaryResourceCache}.
- *
- * @param primary resource
- * @param freshResourceWithStatus - fresh resource with target state
- * @param context of reconciliation
- * @param cache - resource cache managed by user
- * @return the updated resource.
- * @param
primary resource type
- */
- public static
P ssaPatchAndCacheStatus(
- P primary, P freshResourceWithStatus, Context
P ssaPatchAndCacheStatus(
}
/**
- * Patches the resource with JSON Patch and adds it to the {@link PrimaryResourceCache}.
+ * Same as {@link #updateAndCacheResource(HasMetadata, Context, UnaryOperator, UnaryOperator, int,
+ * long,long)} using the default maximum retry number as defined by {@link #DEFAULT_MAX_RETRY} and
+ * default cache maximum polling time and period as defined, respectively by {@link
+ * #DEFAULT_RESOURCE_CACHE_TIMEOUT_MILLIS} and {@link #DEFAULT_RESOURCE_CACHE_POLL_PERIOD_MILLIS}.
*
- * @param primary resource
+ * @param resourceToUpdate original resource to update
* @param context of reconciliation
- * @param cache - resource cache managed by user
- * @return the updated resource.
- * @param
primary resource type
+ * @param modificationFunction modifications to make on primary
+ * @param updateMethod the update method implementation
+ * @param
primary type
+ * @return the updated resource
*/
- public static
P updateAndCacheResource(
+ P resourceToUpdate,
+ Context
context,
+ UnaryOperator
modificationFunction,
+ UnaryOperator
updateMethod) {
+ return updateAndCacheResource(
+ resourceToUpdate,
+ context,
+ modificationFunction,
+ updateMethod,
+ DEFAULT_MAX_RETRY,
+ DEFAULT_RESOURCE_CACHE_TIMEOUT_MILLIS,
+ DEFAULT_RESOURCE_CACHE_POLL_PERIOD_MILLIS);
}
/**
- * Patches the resource with JSON Merge patch and adds it to the {@link PrimaryResourceCache}
- * provided.
+ * Modifies the primary using the specified modification function, then uses the modified resource
+ * for the request to update with provided update method. As the {@code resourceVersion} field of
+ * the modified resource is set to the value found in the specified resource to update, the update
+ * operation will therefore use optimistic locking on the server. If the request fails on
+ * optimistic update, we read the resource again from the K8S API server and retry the whole
+ * process. In short, we make sure we always update the resource with optimistic locking, then we
+ * cache the resource in an internal cache. Without further going into details, the optimistic
+ * locking is needed so we can reliably handle the caching.
*
- * @param primary resource
+ * @param resourceToUpdate original resource to update
* @param context of reconciliation
- * @param cache - resource cache managed by user
- * @return the updated resource.
- * @param
primary resource type
+ * @param modificationFunction modifications to make on primary
+ * @param updateMethod the update method implementation
+ * @param maxRetry maximum number of retries before giving up
+ * @param cachePollTimeoutMillis maximum amount of milliseconds to wait for the updated resource
+ * to appear in cache
+ * @param cachePollPeriodMillis cache polling period, in milliseconds
+ * @param
primary type
+ * @return the updated resource
*/
- public static
P patchAndCacheStatus(
- P primary, Context
context, PrimaryResourceCache
cache) {
- logWarnIfResourceVersionPresent(primary);
- return patchAndCacheStatus(
- primary, cache, () -> context.getClient().resource(primary).patchStatus());
- }
-
- /**
- * Updates the resource and adds it to the {@link PrimaryResourceCache}.
- *
- * @param primary resource
- * @param context of reconciliation
- * @param cache - resource cache managed by user
- * @return the updated resource.
- * @param
primary resource type
- */
- public static
P updateAndCacheStatus(
- P primary, Context
context, PrimaryResourceCache
cache) {
- logWarnIfResourceVersionPresent(primary);
- return patchAndCacheStatus(
- primary, cache, () -> context.getClient().resource(primary).updateStatus());
- }
-
- /**
- * Updates the resource using the user provided implementation anc caches the result.
- *
- * @param primary resource
- * @param cache resource cache managed by user
- * @param patch implementation of resource update*
- * @return the updated resource.
- * @param
primary resource type
- */
- public static
P patchAndCacheStatus(
- P primary, PrimaryResourceCache
cache, Supplier
patch) {
- var updatedResource = patch.get();
- cache.cacheResource(primary, updatedResource);
- return updatedResource;
+ public static
P updateAndCacheResource(
+ P resourceToUpdate,
+ Context
context,
+ UnaryOperator
modificationFunction,
+ UnaryOperator
updateMethod,
+ int maxRetry,
+ long cachePollTimeoutMillis,
+ long cachePollPeriodMillis) {
+
+ if (log.isDebugEnabled()) {
+ log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resourceToUpdate));
+ }
+ P modified = null;
+ int retryIndex = 0;
+ while (true) {
+ try {
+ modified = modificationFunction.apply(resourceToUpdate);
+ modified
+ .getMetadata()
+ .setResourceVersion(resourceToUpdate.getMetadata().getResourceVersion());
+ var updated = updateMethod.apply(modified);
+ context
+ .eventSourceRetriever()
+ .getControllerEventSource()
+ .handleRecentResourceUpdate(
+ ResourceID.fromResource(resourceToUpdate), updated, resourceToUpdate);
+ return updated;
+ } catch (KubernetesClientException e) {
+ log.trace("Exception during patch for resource: {}", resourceToUpdate);
+ retryIndex++;
+ // only retry on conflict (409) and unprocessable content (422) which
+ // can happen if JSON Patch is not a valid request since there was
+ // a concurrent request which already removed another finalizer:
+ // List element removal from a list is by index in JSON Patch
+ // so if addressing a second finalizer but first is meanwhile removed
+ // it is a wrong request.
+ if (e.getCode() != 409 && e.getCode() != 422) {
+ throw e;
+ }
+ if (retryIndex > maxRetry) {
+ log.warn("Retry exhausted, last desired resource: {}", modified);
+ throw new OperatorException(
+ "Exceeded maximum ("
+ + maxRetry
+ + ") retry attempts to patch resource: "
+ + ResourceID.fromResource(resourceToUpdate),
+ e);
+ }
+ log.debug(
+ "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}",
+ resourceToUpdate.getMetadata().getName(),
+ resourceToUpdate.getMetadata().getNamespace(),
+ e.getCode());
+ resourceToUpdate =
+ pollLocalCache(
+ context, resourceToUpdate, cachePollTimeoutMillis, cachePollPeriodMillis);
+ }
+ }
}
- private static
void logWarnIfResourceVersionPresent(P primary) {
- if (primary.getMetadata().getResourceVersion() != null) {
- log.warn(
- "The metadata.resourceVersion of primary resource is NOT null, "
- + "using optimistic locking is discouraged for this purpose. ");
+ private static
P pollLocalCache(
+ Context
context, P staleResource, long timeoutMillis, long pollDelayMillis) {
+ try {
+ var resourceId = ResourceID.fromResource(staleResource);
+ var startTime = LocalTime.now();
+ final var timeoutTime = startTime.plus(timeoutMillis, ChronoUnit.MILLIS);
+ while (timeoutTime.isAfter(LocalTime.now())) {
+ log.debug("Polling cache for resource: {}", resourceId);
+ var cachedResource = context.getPrimaryCache().get(resourceId).orElseThrow();
+ if (!cachedResource
+ .getMetadata()
+ .getResourceVersion()
+ .equals(staleResource.getMetadata().getResourceVersion())) {
+ return context
+ .getControllerConfiguration()
+ .getConfigurationService()
+ .getResourceCloner()
+ .clone(cachedResource);
+ }
+ Thread.sleep(pollDelayMillis);
+ }
+ throw new OperatorException("Timeout of resource polling from cache for resource");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new OperatorException(e);
}
}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java
index 1b5eefd7ff..1bd98c12d6 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java
@@ -21,8 +21,7 @@ private UpdateControl(P resource, boolean patchResource, boolean patchStatus) {
}
/**
- * Preferred way to update the status. It does not do optimistic locking. Uses JSON Patch to patch
- * the resource.
+ * Preferred way to update the status. Uses JSON Patch to patch the resource.
*
*
Note that this does not work, if the {@link CustomResource#initStatus()} is implemented,
* since it breaks the diffing process. Don't implement it if using this method. There is also an
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/support/PrimaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/support/PrimaryResourceCache.java
deleted file mode 100644
index 4da73ab8b1..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/support/PrimaryResourceCache.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package io.javaoperatorsdk.operator.api.reconciler.support;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.BiPredicate;
-
-import io.fabric8.kubernetes.api.model.HasMetadata;
-import io.javaoperatorsdk.operator.processing.event.ResourceID;
-
-public class PrimaryResourceCache
{
-
- private final BiPredicate, P> evictionPredicate;
- private final ConcurrentHashMap> cache = new ConcurrentHashMap<>();
-
- public PrimaryResourceCache(BiPredicate, P> evictionPredicate) {
- this.evictionPredicate = evictionPredicate;
- }
-
- public PrimaryResourceCache() {
- this(new ResourceVersionParsingEvictionPredicate<>());
- }
-
- public void cacheResource(P afterUpdate) {
- var resourceId = ResourceID.fromResource(afterUpdate);
- cache.put(resourceId, new Pair<>(null, afterUpdate));
- }
-
- public void cacheResource(P beforeUpdate, P afterUpdate) {
- var resourceId = ResourceID.fromResource(beforeUpdate);
- cache.put(resourceId, new Pair<>(beforeUpdate, afterUpdate));
- }
-
- public P getFreshResource(P newVersion) {
- var resourceId = ResourceID.fromResource(newVersion);
- var pair = cache.get(resourceId);
- if (pair == null) {
- return newVersion;
- }
- if (!newVersion.getMetadata().getUid().equals(pair.afterUpdate().getMetadata().getUid())) {
- cache.remove(resourceId);
- return newVersion;
- }
- if (evictionPredicate.test(pair, newVersion)) {
- cache.remove(resourceId);
- return newVersion;
- } else {
- return pair.afterUpdate();
- }
- }
-
- public void cleanup(P resource) {
- cache.remove(ResourceID.fromResource(resource));
- }
-
- public record Pair(T beforeUpdate, T afterUpdate) {}
-
- /** This works in general, but it does not strictly follow the contract with k8s API */
- public static class ResourceVersionParsingEvictionPredicate
- implements BiPredicate, T> {
- @Override
- public boolean test(Pair updatePair, T newVersion) {
- return Long.parseLong(updatePair.afterUpdate().getMetadata().getResourceVersion())
- <= Long.parseLong(newVersion.getMetadata().getResourceVersion());
- }
- }
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java
index 5cee9467f1..7f2674892f 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java
@@ -3,6 +3,7 @@
import java.util.Optional;
import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.api.config.Utils;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.Ignore;
@@ -23,13 +24,22 @@ public abstract class AbstractEventSourceHolderDependentResource<
private boolean isCacheFillerEventSource;
protected String eventSourceNameToUse;
+ @SuppressWarnings("unchecked")
+ protected AbstractEventSourceHolderDependentResource() {
+ this(null, null);
+ }
+
protected AbstractEventSourceHolderDependentResource(Class resourceType) {
this(resourceType, null);
}
protected AbstractEventSourceHolderDependentResource(Class resourceType, String name) {
super(name);
- this.resourceType = resourceType;
+ if (resourceType == null) {
+ this.resourceType = (Class) Utils.getTypeArgumentFromHierarchyByIndex(getClass(), 0);
+ } else {
+ this.resourceType = resourceType;
+ }
}
/**
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java
index 1148895709..4c828b7eb9 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java
@@ -21,6 +21,8 @@ public abstract class AbstractExternalDependentResource<
private InformerEventSource, P> externalStateEventSource;
+ protected AbstractExternalDependentResource() {}
+
@SuppressWarnings("unchecked")
protected AbstractExternalDependentResource(Class resourceType) {
super(resourceType);
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java
index 659b8b4720..3cf93cba53 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java
@@ -16,6 +16,8 @@ public abstract class AbstractPollingDependentResource
public static final Duration DEFAULT_POLLING_PERIOD = Duration.ofMillis(5000);
private Duration pollingPeriod;
+ protected AbstractPollingDependentResource() {}
+
protected AbstractPollingDependentResource(Class resourceType) {
this(resourceType, DEFAULT_POLLING_PERIOD);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java
index 8cbe9f48d5..c0181207d8 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java
@@ -14,6 +14,8 @@ public abstract class PerResourcePollingDependentResource
implements PerResourcePollingEventSource.ResourceFetcher {
+ public PerResourcePollingDependentResource() {}
+
public PerResourcePollingDependentResource(Class resourceType) {
super(resourceType);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java
index afe4302fc3..392ac6d894 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java
@@ -18,6 +18,8 @@ public abstract class CRUDKubernetesDependentResource
implements Creator, Updater, GarbageCollected
{
+ public CRUDKubernetesDependentResource() {}
+
public CRUDKubernetesDependentResource(Class resourceType) {
super(resourceType);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java
index 549f26437a..3b3c11b006 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java
@@ -20,6 +20,8 @@
public class CRUDNoGCKubernetesDependentResource
extends KubernetesDependentResource implements Creator, Updater, Deleter
{
+ public CRUDNoGCKubernetesDependentResource() {}
+
public CRUDNoGCKubernetesDependentResource(Class resourceType) {
super(resourceType);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
index ea7edbc1a0..ebd6089aa7 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
@@ -41,6 +41,9 @@ public abstract class KubernetesDependentResource kubernetesDependentResourceConfig;
private volatile Boolean useSSA;
+ private volatile Boolean usePreviousAnnotationForEventFiltering;
+
+ public KubernetesDependentResource() {}
public KubernetesDependentResource(Class resourceType) {
this(resourceType, null);
@@ -163,10 +166,19 @@ protected boolean useSSA(Context
context) {
- return context
- .getControllerConfiguration()
- .getConfigurationService()
- .previousAnnotationForDependentResourcesEventFiltering();
+ if (usePreviousAnnotationForEventFiltering == null) {
+ usePreviousAnnotationForEventFiltering =
+ context
+ .getControllerConfiguration()
+ .getConfigurationService()
+ .previousAnnotationForDependentResourcesEventFiltering()
+ && !context
+ .getControllerConfiguration()
+ .getConfigurationService()
+ .withPreviousAnnotationForDependentResourcesBlocklist()
+ .contains(this.resourceType());
+ }
+ return usePreviousAnnotationForEventFiltering;
}
@Override
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java
index bcfe2f9fe6..6f626d2628 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java
@@ -12,6 +12,13 @@ public class KubernetesDependentResourceConfig {
private final InformerConfiguration informerConfig;
private final SSABasedGenericKubernetesResourceMatcher matcher;
+ public KubernetesDependentResourceConfig(
+ Boolean useSSA,
+ boolean createResourceOnlyIfNotExistingWithSSA,
+ InformerConfiguration informerConfig) {
+ this(useSSA, createResourceOnlyIfNotExistingWithSSA, informerConfig, null);
+ }
+
public KubernetesDependentResourceConfig(
Boolean useSSA,
boolean createResourceOnlyIfNotExistingWithSSA,
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java
similarity index 57%
rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizer.java
rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java
index 7193085b63..962059961e 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizer.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java
@@ -2,32 +2,34 @@
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
/**
- * Sanitizes the {@link ResourceRequirements} in the containers of a pair of {@link PodTemplateSpec}
- * instances.
+ * Sanitizes the {@link ResourceRequirements} and the {@link EnvVar} in the containers of a pair of
+ * {@link PodTemplateSpec} instances.
*
*
When the sanitizer finds a mismatch in the structure of the given templates, before it gets to
- * the nested resource limits and requests, it returns early without fixing the actual map. This is
- * an optimization because the given templates will anyway differ at this point. This means we do
- * not have to attempt to sanitize the resources for these use cases, since there will anyway be an
- * update of the K8s resource.
+ * the nested fields, it returns early without fixing the actual map. This is an optimization
+ * because the given templates will anyway differ at this point. This means we do not have to
+ * attempt to sanitize the fields for these use cases, since there will anyway be an update of the
+ * K8s resource.
*
*
The algorithm traverses the whole template structure because we need the actual and desired
- * {@link Quantity} instances to compare their numerical amount. Using the {@link
+ * {@link Quantity} and {@link EnvVar} instances. Using the {@link
* GenericKubernetesResource#get(Map, Object...)} shortcut would need to create new instances just
* for the sanitization check.
*/
-class ResourceRequirementsSanitizer {
+class PodTemplateSpecSanitizer {
- static void sanitizeResourceRequirements(
+ static void sanitizePodTemplateSpec(
final Map actualMap,
final PodTemplateSpec actualTemplate,
final PodTemplateSpec desiredTemplate) {
@@ -37,19 +39,19 @@ static void sanitizeResourceRequirements(
if (actualTemplate.getSpec() == null || desiredTemplate.getSpec() == null) {
return;
}
- sanitizeResourceRequirements(
+ sanitizePodTemplateSpec(
actualMap,
actualTemplate.getSpec().getInitContainers(),
desiredTemplate.getSpec().getInitContainers(),
"initContainers");
- sanitizeResourceRequirements(
+ sanitizePodTemplateSpec(
actualMap,
actualTemplate.getSpec().getContainers(),
desiredTemplate.getSpec().getContainers(),
"containers");
}
- private static void sanitizeResourceRequirements(
+ private static void sanitizePodTemplateSpec(
final Map actualMap,
final List actualContainers,
final List desiredContainers,
@@ -57,11 +59,17 @@ private static void sanitizeResourceRequirements(
int containers = desiredContainers.size();
if (containers == actualContainers.size()) {
for (int containerIndex = 0; containerIndex < containers; containerIndex++) {
- var desiredContainer = desiredContainers.get(containerIndex);
- var actualContainer = actualContainers.get(containerIndex);
+ final var desiredContainer = desiredContainers.get(containerIndex);
+ final var actualContainer = actualContainers.get(containerIndex);
if (!desiredContainer.getName().equals(actualContainer.getName())) {
return;
}
+ sanitizeEnvVars(
+ actualMap,
+ actualContainer.getEnv(),
+ desiredContainer.getEnv(),
+ containerPath,
+ containerIndex);
sanitizeResourceRequirements(
actualMap,
actualContainer.getResources(),
@@ -121,7 +129,7 @@ private static void sanitizeQuantities(
m ->
actualResource.forEach(
(key, actualQuantity) -> {
- var desiredQuantity = desiredResource.get(key);
+ final var desiredQuantity = desiredResource.get(key);
if (desiredQuantity == null) {
return;
}
@@ -138,4 +146,53 @@ private static void sanitizeQuantities(
}
}));
}
+
+ @SuppressWarnings("unchecked")
+ private static void sanitizeEnvVars(
+ final Map actualMap,
+ final List actualEnvVars,
+ final List desiredEnvVars,
+ final String containerPath,
+ final int containerIndex) {
+ if (desiredEnvVars.isEmpty() || actualEnvVars.isEmpty()) {
+ return;
+ }
+ Optional.ofNullable(
+ GenericKubernetesResource.get(
+ actualMap, "spec", "template", "spec", containerPath, containerIndex, "env"))
+ .map(List.class::cast)
+ .ifPresent(
+ envVars ->
+ actualEnvVars.forEach(
+ actualEnvVar -> {
+ final var actualEnvVarName = actualEnvVar.getName();
+ final var actualEnvVarValue = actualEnvVar.getValue();
+ // check if the actual EnvVar value string is not null or the desired EnvVar
+ // already contains the same EnvVar name with a non empty EnvVar value
+ final var isDesiredEnvVarEmpty =
+ hasEnvVarNoEmptyValue(actualEnvVarName, desiredEnvVars);
+ if (actualEnvVarValue != null || isDesiredEnvVarEmpty) {
+ return;
+ }
+ envVars.stream()
+ .filter(
+ envVar ->
+ ((Map) envVar)
+ .get("name")
+ .equals(actualEnvVarName))
+ // add the actual EnvVar value with an empty string to prevent a
+ // resource update
+ .forEach(envVar -> ((Map) envVar).put("value", ""));
+ }));
+ }
+
+ private static boolean hasEnvVarNoEmptyValue(
+ final String envVarName, final List envVars) {
+ return envVars.stream()
+ .anyMatch(
+ envVar ->
+ Objects.equals(envVarName, envVar.getName())
+ && envVar.getValue() != null
+ && !envVar.getValue().isEmpty());
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java
deleted file mode 100644
index d893ff3e86..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
-
-import io.fabric8.kubernetes.api.model.HasMetadata;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public interface ResourceUpdaterMatcher {
-
- R updateResource(R actual, R desired, Context> context);
-
- boolean matches(R actual, R desired, Context> context);
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
index 3c051acfb4..4954dfd17a 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
@@ -31,7 +31,7 @@
import com.github.difflib.DiffUtils;
import com.github.difflib.UnifiedDiffUtils;
-import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceRequirementsSanitizer.sanitizeResourceRequirements;
+import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.PodTemplateSpecSanitizer.sanitizePodTemplateSpec;
/**
* Matches the actual state on the server vs the desired state. Based on the managedFields of SSA.
@@ -179,7 +179,7 @@ private Optional checkIfFieldManagerExists(R actual, String
}
/** Correct for known issue with SSA */
- private void sanitizeState(R actual, R desired, Map actualMap) {
+ protected void sanitizeState(R actual, R desired, Map actualMap) {
if (actual instanceof StatefulSet actualStatefulSet
&& desired instanceof StatefulSet desiredStatefulSet) {
var actualSpec = actualStatefulSet.getSpec();
@@ -203,22 +203,22 @@ private void sanitizeState(R actual, R desired, Map actualMap) {
}
}
}
- sanitizeResourceRequirements(actualMap, actualSpec.getTemplate(), desiredSpec.getTemplate());
+ sanitizePodTemplateSpec(actualMap, actualSpec.getTemplate(), desiredSpec.getTemplate());
} else if (actual instanceof Deployment actualDeployment
&& desired instanceof Deployment desiredDeployment) {
- sanitizeResourceRequirements(
+ sanitizePodTemplateSpec(
actualMap,
actualDeployment.getSpec().getTemplate(),
desiredDeployment.getSpec().getTemplate());
} else if (actual instanceof ReplicaSet actualReplicaSet
&& desired instanceof ReplicaSet desiredReplicaSet) {
- sanitizeResourceRequirements(
+ sanitizePodTemplateSpec(
actualMap,
actualReplicaSet.getSpec().getTemplate(),
desiredReplicaSet.getSpec().getTemplate());
} else if (actual instanceof DaemonSet actualDaemonSet
&& desired instanceof DaemonSet desiredDaemonSet) {
- sanitizeResourceRequirements(
+ sanitizePodTemplateSpec(
actualMap,
actualDaemonSet.getSpec().getTemplate(),
desiredDaemonSet.getSpec().getTemplate());
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java
index 065e790ba4..9e29305b51 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java
@@ -144,9 +144,11 @@ protected void doRun(DependentResourceNode dependentResourceNode) {
log.debug("Reconciling for primary: {} node: {} ", primaryID, dependentResourceNode);
ReconcileResult reconcileResult = dependentResource.reconcile(primary, context);
final var detailBuilder = createOrGetResultFor(dependentResourceNode);
- detailBuilder.withReconcileResult(reconcileResult).markAsVisited();
- if (isConditionMet(dependentResourceNode.getReadyPostcondition(), dependentResourceNode)) {
+ boolean isReadyPostconditionMet =
+ isConditionMet(dependentResourceNode.getReadyPostcondition(), dependentResourceNode);
+ detailBuilder.withReconcileResult(reconcileResult).markAsVisited();
+ if (isReadyPostconditionMet) {
log.debug(
"Setting already reconciled for: {} primaryID: {}", dependentResourceNode, primaryID);
handleDependentsReconcile(dependentResourceNode);
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java
index bdaf575814..e029e287a0 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java
@@ -102,8 +102,16 @@ public synchronized void handleEvent(Event event) {
try {
log.debug("Received event: {}", event);
+ final var optionalState = resourceStateManager.getOrCreateOnResourceEvent(event);
+ if (optionalState.isEmpty()) {
+ log.debug(
+ "Skipping event, since no state present and it is not a resource event. Resource ID:"
+ + " {}",
+ event.getRelatedCustomResourceID());
+ return;
+ }
+ var state = optionalState.orElseThrow();
final var resourceID = event.getRelatedCustomResourceID();
- final var state = resourceStateManager.getOrCreate(event.getRelatedCustomResourceID());
MDCUtils.addResourceIDInfo(resourceID);
metrics.receivedEvent(event, metricsMetadata);
handleEventMarking(event, state);
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java
index 6932e1ca5e..481fd317ff 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java
@@ -2,15 +2,33 @@
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
+import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent;
+
class ResourceStateManager {
// maybe we should have a way for users to specify a hint on the amount of CRs their reconciler
// will process to avoid under- or over-sizing the state maps and avoid too many resizing that
// take time and memory?
private final Map states = new ConcurrentHashMap<>(100);
+ public Optional getOrCreateOnResourceEvent(Event event) {
+ var resourceId = event.getRelatedCustomResourceID();
+ var state = states.get(event.getRelatedCustomResourceID());
+ if (state != null) {
+ return Optional.of(state);
+ }
+ if (event instanceof ResourceEvent) {
+ state = new ResourceState(resourceId);
+ states.put(resourceId, state);
+ return Optional.of(state);
+ } else {
+ return Optional.empty();
+ }
+ }
+
public ResourceState getOrCreate(ResourceID resourceID) {
return states.computeIfAbsent(resourceID, ResourceState::new);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java
index b52dc278f2..c029a54170 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java
@@ -95,7 +95,7 @@ private InformerEventSource(
parseResourceVersions);
// If there is a primary to secondary mapper there is no need for primary to secondary index.
primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper();
- if (primaryToSecondaryMapper == null) {
+ if (useSecondaryToPrimaryIndex()) {
primaryToSecondaryIndex =
// The index uses the secondary to primary mapper (always present) to build the index
new DefaultPrimaryToSecondaryIndex<>(configuration.getSecondaryToPrimaryMapper());
@@ -157,6 +157,14 @@ public void onDelete(R resource, boolean b) {
}
}
+ @Override
+ public synchronized void start() {
+ super.start();
+ // this makes sure that on first reconciliation all resources are
+ // present on the index
+ manager().list().forEach(primaryToSecondaryIndex::onAddOrUpdate);
+ }
+
private synchronized void onAddOrUpdate(
Operation operation, R newObject, R oldObject, Runnable superOnOp) {
var resourceID = ResourceID.fromResource(newObject);
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java
index 9ec5b3694c..af75a5abc4 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java
@@ -126,9 +126,7 @@ public synchronized void putResource(T newResource, String previousResourceVersi
knownResourceVersions.add(newResource.getMetadata().getResourceVersion());
}
var resourceId = ResourceID.fromResource(newResource);
- var cachedResource =
- getResourceFromCache(resourceId)
- .orElse(managedInformerEventSource.get(resourceId).orElse(null));
+ var cachedResource = managedInformerEventSource.get(resourceId).orElse(null);
boolean moveAhead = false;
if (previousResourceVersion == null && cachedResource == null) {
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java
index 2467df75aa..4f30458d68 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java
@@ -3,12 +3,14 @@
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
import org.junit.jupiter.api.Test;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class ConfigurationServiceOverriderTest {
@@ -26,30 +28,32 @@ public R clone(R object) {
}
};
+ final BaseConfigurationService config =
+ new BaseConfigurationService(null) {
+ @Override
+ public boolean checkCRDAndValidateLocalModel() {
+ return false;
+ }
+
+ @Override
+ public Metrics getMetrics() {
+ return METRICS;
+ }
+
+ @Override
+ public Cloner getResourceCloner() {
+ return CLONER;
+ }
+
+ @Override
+ public Optional getLeaderElectionConfiguration() {
+ return Optional.of(LEADER_ELECTION_CONFIGURATION);
+ }
+ };
+
@Test
void overrideShouldWork() {
- final var config =
- new BaseConfigurationService(null) {
- @Override
- public boolean checkCRDAndValidateLocalModel() {
- return false;
- }
-
- @Override
- public Metrics getMetrics() {
- return METRICS;
- }
-
- @Override
- public Cloner getResourceCloner() {
- return CLONER;
- }
-
- @Override
- public Optional getLeaderElectionConfiguration() {
- return Optional.of(LEADER_ELECTION_CONFIGURATION);
- }
- };
+
final var overridden =
new ConfigurationServiceOverrider(config)
.checkingCRDAndValidateLocalModel(true)
@@ -86,4 +90,17 @@ public R clone(R object) {
assertNotEquals(
config.reconciliationTerminationTimeout(), overridden.reconciliationTerminationTimeout());
}
+
+ @Test
+ void threadCountConfiguredProperly() {
+ final var overridden =
+ new ConfigurationServiceOverrider(config)
+ .withConcurrentReconciliationThreads(13)
+ .withConcurrentWorkflowExecutorThreads(14)
+ .build();
+ assertThat(((ThreadPoolExecutor) overridden.getExecutorService()).getMaximumPoolSize())
+ .isEqualTo(13);
+ assertThat(((ThreadPoolExecutor) overridden.getWorkflowExecutorService()).getMaximumPoolSize())
+ .isEqualTo(14);
+ }
}
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java
index 33191a8141..837ad7463a 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java
@@ -89,6 +89,22 @@ private io.javaoperatorsdk.operator.api.config.ControllerConfiguration> create
return configurationService.configFor(reconciler);
}
+ @Test
+ void overridingNamespacesShouldNotThrowNPE() {
+ var configuration = createConfiguration(new NullReconciler());
+ configuration =
+ ControllerConfigurationOverrider.override(configuration).settingNamespaces().build();
+ assertTrue(configuration.getInformerConfig().watchAllNamespaces());
+ }
+
+ private static class NullReconciler implements Reconciler {
+ @Override
+ public UpdateControl reconcile(HasMetadata resource, Context context)
+ throws Exception {
+ return null;
+ }
+ }
+
@Test
void overridingNamespacesShouldWork() {
var configuration = createConfiguration(new WatchCurrentReconciler());
@@ -359,21 +375,11 @@ public UpdateControl reconcile(ConfigMap resource, Context
}
public static class ReadOnlyDependent extends KubernetesDependentResource
- implements GarbageCollected {
-
- public ReadOnlyDependent() {
- super(ConfigMap.class);
- }
- }
+ implements GarbageCollected {}
@KubernetesDependent(informer = @Informer(namespaces = Constants.WATCH_ALL_NAMESPACES))
public static class WatchAllNSDependent extends KubernetesDependentResource
- implements GarbageCollected {
-
- public WatchAllNSDependent() {
- super(ConfigMap.class);
- }
- }
+ implements GarbageCollected {}
@Workflow(dependents = @Dependent(type = OverriddenNSDependent.class))
@ControllerConfiguration(
@@ -394,10 +400,6 @@ public static class OverriddenNSDependent
implements GarbageCollected {
private static final String DEP_NS = "dependentNS";
-
- public OverriddenNSDependent() {
- super(ConfigMap.class);
- }
}
@Workflow(
@@ -415,12 +417,7 @@ public UpdateControl reconcile(ConfigMap resource, Context
private static class NamedDependentResource
extends KubernetesDependentResource
- implements GarbageCollected {
-
- public NamedDependentResource() {
- super(ConfigMap.class);
- }
- }
+ implements GarbageCollected {}
private static class ExternalDependentResource
implements DependentResource