Skip to content

Commit 357d601

Browse files
committed
Merge branch 'main' into next
# Conflicts: # micrometer-support/pom.xml # operator-framework-core/pom.xml # operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java # operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java # operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java # operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java # operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java # operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java # operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java # operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java # operator-framework-junit5/pom.xml # operator-framework/pom.xml # operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java # pom.xml # sample-operators/mysql-schema/pom.xml # sample-operators/pom.xml # sample-operators/tomcat-operator/pom.xml # sample-operators/webpage/pom.xml # smoke-test-samples/common/pom.xml # smoke-test-samples/pom.xml # smoke-test-samples/pure-java/pom.xml # smoke-test-samples/spring-boot-plain/pom.xml
2 parents b282d4e + 6f8d38d commit 357d601

File tree

56 files changed

+783
-153
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+783
-153
lines changed

.github/workflows/pr.yml

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,48 @@ on:
1111
branches: [ main, v1, next ]
1212
workflow_dispatch:
1313
jobs:
14-
build:
14+
check_format_and_unit_tests:
1515
runs-on: ubuntu-latest
16-
strategy:
17-
matrix:
18-
java: [ 11, 17 ]
19-
distribution: [ temurin ]
20-
kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1', 'v1.23.0' ]
2116
steps:
2217
- uses: actions/checkout@v2
2318
- name: Set up Java and Maven
2419
uses: actions/setup-java@v2
2520
with:
26-
distribution: ${{ matrix.distribution }}
27-
java-version: ${{ matrix.java }}
21+
distribution: temurin
22+
java-version: 17
2823
cache: 'maven'
2924
- name: Check code format
3025
run: |
3126
./mvnw ${MAVEN_ARGS} formatter:validate -Dconfigfile=$PWD/contributing/eclipse-google-style.xml --file pom.xml
3227
./mvnw ${MAVEN_ARGS} impsort:check --file pom.xml
3328
- name: Run unit tests
3429
run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml
30+
31+
integration_tests:
32+
runs-on: ubuntu-latest
33+
strategy:
34+
matrix:
35+
java: [ 11, 17 ]
36+
kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1', 'v1.23.0' ]
37+
exclude:
38+
- java: 11
39+
kubernetes: 'v1.18.20'
40+
- java: 11
41+
kubernetes: 'v1.19.14'
42+
- java: 11
43+
kubernetes: 'v1.20.10'
44+
- java: 11
45+
kubernetes: 'v1.21.4'
46+
- java: 11
47+
kubernetes: 'v1.22.1'
48+
steps:
49+
- uses: actions/checkout@v2
50+
- name: Set up Java and Maven
51+
uses: actions/setup-java@v2
52+
with:
53+
distribution: temurin
54+
java-version: ${{ matrix.java }}
55+
cache: 'maven'
3556
- name: Set up Minikube
3657
uses: manusa/[email protected]
3758
with:

.github/workflows/sonar.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ on:
1515
jobs:
1616
test:
1717
runs-on: ubuntu-latest
18+
if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'java-operator-sdk' ) }}
1819
steps:
1920
- uses: actions/checkout@v2
2021
- name: Set up Java and Maven

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,5 @@ Operator SDK plugin: https://github.com/operator-framework/java-operator-plugins
4444

4545
Quarkus Extension: https://github.com/quarkiverse/quarkus-operator-sdk
4646

47-
47+
Spring Boot Starter: https://github.com/java-operator-sdk/operator-framework-spring-boot-starter
4848

docs/_includes/hero.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
{% endif %}
66
<h1 class="uk-heading-hero uk-text-bold uk-text-uppercase uk-text-muted uk-inline " style="font-family: 'Asap', sans-serif"> [ <small class="uk-margin-small-right uk-margin-small-left"> {{ site.title }} </small> ]</h1>
77
<div class="links uk-container uk-margin-medium-top">
8-
<a class="uk-button uk-button-small uk-border-pill uk-button-secondary uk-margin-small-bottom" href="{{ domain }}{{ link.url }}">
8+
<a class="uk-button uk-button-small uk-border-pill uk-button-secondary uk-margin-small-bottom" href="/docs/getting-started">
99
Get Started
1010
</a>
11-
<a class="uk-button uk-button-small uk-border-pill uk-button-default uk-margin-small-bottom " href="{{ domain }}{{ link.url }}">
11+
<a class="uk-button uk-button-small uk-border-pill uk-button-default uk-margin-small-bottom " href="/docs/contributing">
1212
<span uk-icon="github"></span> Contribute
1313
</a>
14-
<a class="uk-button uk-button-small uk-border-pill uk-button-default uk-margin-small-bottom" href="{{ domain }}{{ link.url }}">
14+
<a class="uk-button uk-button-small uk-border-pill uk-button-default uk-margin-small-bottom" href="https://discord.gg/DacEhAy">
1515
<span uk-icon="discord"></span> Discord Channel
1616
</a>
1717
</div>

docs/documentation/features.md

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,13 @@ mostly for the cases when there is a long waiting period after a delete operatio
137137
you might want to either schedule a timed event to make sure
138138
`cleanup` is executed again or use event sources to get notified about the state changes of a deleted resource.
139139

140-
## Generation Awareness and Automatic Observed Generation Handling
140+
## Automatic Observed Generation Handling
141141

142142
Having `.observedGeneration` value on the status of the resource is a best practice to indicate the last generation of
143143
the resource reconciled successfully by the controller. This helps the users / administrators to check if the custom
144-
resource was reconciled, but it is used to decide if a reconciliation should happen or not. Filtering events based on
145-
generation is supported by the framework and turned on by default. There are two modes.
144+
resource was reconciled.
146145

147-
### Primary (preferred) Mode
148-
149-
The first and the **preferred** one is to check after a resource event received, if the generation of the resource is
150-
larger than the `.observedGeneration` field on status. In order to have this feature working:
146+
In order to have this feature working:
151147

152148
- the **status class** (not the resource) must implement the
153149
[`ObservedGenerationAware`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java)
@@ -160,9 +156,7 @@ larger than the `.observedGeneration` field on status. In order to have this fea
160156
.
161157

162158
If these conditions are fulfilled and generation awareness not turned off, the observed generation is automatically set
163-
by the framework after the `reconcile` method is called. There is just one exception, when the reconciler returns
164-
with `UpdateControl.updateResource`, in this case the status is not updated, just the custom resource - however, this
165-
update will lead to a new reconciliation. Note that the observed generation is updated also
159+
by the framework after the `reconcile` method is called. Note that the observed generation is updated also
166160
when `UpdateControl.noUpdate()` is returned from the reconciler. See this feature working in
167161
the [WebPage example](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java#L5)
168162
.
@@ -191,15 +185,18 @@ public class WebPage extends CustomResource<WebPageSpec, WebPageStatus>
191185
}
192186
```
193187

194-
### The Second (Fallback) Mode
188+
## Generation Awareness and Event Filtering
189+
190+
On an operator startup, the best practice is to reconcile all the resources. Since while operator was down, changes
191+
might have made both to custom resource and dependent resources.
195192

196-
The second, fallback mode is (when the conditions from above are not met to handle the observed generation automatically
197-
in status) to handle generation filtering in memory. Thus, if an event is received, the generation of the received
198-
resource is compared with the resource in the cache.
193+
When the first reconciliation is done successfully, the next reconciliation is triggered if either the dependent
194+
resources are changed or the custom resource `.spec` is changed. If other fields like `.metadata` is changed on the
195+
custom resource, the reconciliation could be skipped. This is supported out of the box, thus the reconciliation by
196+
default is not triggered if the change to the main custom resource does not increase the `.metadata.generation` field.
197+
Note that the increase of `.metada.generation` is handled automatically by Kubernetes.
199198

200-
Note that the **first approach has significant benefits** in the situation when the operator is restarted and there is
201-
no cached resource anymore. In case two this leads to a reconciliation of every resource, event if the resource is not
202-
changed while the operator was not running.
199+
To turn on this feature set `generationAwareEventProcessing` to `false` for the `Reconciler`.
203200

204201
## Support for Well Known (non-custom) Kubernetes Resources
205202

@@ -223,6 +220,30 @@ public class DeploymentReconciler
223220
}
224221
```
225222

223+
## Max Interval Between Reconciliations
224+
225+
In case informers are all in place and reconciler is implemented correctly, there is no need for additional triggers.
226+
However, it's a [common practice](https://github.com/java-operator-sdk/java-operator-sdk/issues/848#issuecomment-1016419966)
227+
to have a failsafe periodic trigger in place,
228+
just to make sure the resources are reconciled after certain time. This functionality is in place by default, there
229+
is quite high interval (currently 10 hours) while the reconciliation is triggered. See how to override this using
230+
the standard annotation:
231+
232+
```java
233+
@ControllerConfiguration(finalizerName = NO_FINALIZER,
234+
reconciliationMaxInterval = @ReconciliationMaxInterval(
235+
interval = 50,
236+
timeUnit = TimeUnit.MILLISECONDS))
237+
```
238+
239+
The event is not propagated in a fixed rate, rather it's scheduled after each reconciliation. So the
240+
next reconciliation will after at most within the specified interval after last reconciliation.
241+
242+
This feature can be turned off by setting `reconciliationMaxInterval` to [`Constants.NO_RECONCILIATION_MAX_INTERVAL`](https://github.com/java-operator-sdk/java-operator-sdk/blob/442e7d8718e992a36880e42bd0a5c01affaec9df/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java#L8-L8)
243+
or any non-positive number.
244+
245+
The automatic retries are not affected by this feature, in case of an error no schedule is set by this feature.
246+
226247
## Automatic Retries on Error
227248

228249
When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. The

docs/documentation/patterns-best-practices.md

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,79 @@ permalink: /docs/patterns-best-practices
77

88
# Patterns and Best Practices
99

10-
This document describes patters and best practices, to build and run operators, and how to implement them in terms
11-
of Java Operator SDK.
10+
This document describes patterns and best practices, to build and run operators, and how to implement them in terms of
11+
Java Operator SDK.
12+
13+
See also best practices in [Operator SDK](https://sdk.operatorframework.io/docs/best-practices/best-practices/).
1214

1315
## Implementing a Reconciler
1416

17+
### Reconcile All The Resources All the Time
18+
19+
The reconciliation can be triggered by events from multiple sources. It could be tempting to check the events and
20+
reconcile just the related resource or subset of resources that the controller manages. However, this is **considered as
21+
an anti-pattern** in operators. If triggered, all resources should be reconciled. Usually this means only comparing the
22+
target state with the current state in the cache for most of the resource. The reason behind this is events not reliable
23+
In general, this means events can be lost. In addition to that the operator can crash and while down will miss events.
24+
25+
In addition to that such approach might even complicate implementation logic in the `Reconciler`, since parallel
26+
execution of the reconciler is not allowed for the same custom resource, there can be multiple events received for the
27+
same resource or dependent resource during an ongoing execution, ordering those events could be also challenging.
28+
29+
Since there is a consensus regarding this in the industry, from v2 the events are not even accessible for
30+
the `Reconciler`.
31+
32+
### EventSources and Caching
33+
34+
As mentioned above during a reconciliation best practice is to reconcile all the dependent resources managed by the
35+
controller. This means that we want to compare a target state with the actual state of the cluster. Reading the actual
36+
state of a resource from the Kubernetes API Server directly all the time would mean a significant load. Therefore, it's
37+
a common practice to instead create a watch for the dependent resources and cache their latest state. This is done
38+
following the Informer pattern. In Java Operator SDK, informer is wrapped into an EventSource, to integrate it with the
39+
eventing system of the framework, resulting in `InformerEventSource`.
40+
41+
A new event that triggers the reconciliation is propagated when the actual resource is already in cache. So in
42+
reconciler what should be just done is to compare the target calculated state of a dependent resource of the actual
43+
state from the cache of the event source. If it is changed or not in the cache it needs to be created, respectively
44+
updated.
45+
1546
### Idempotency
1647

17-
### Sync of Async Way of Resource Handling
48+
Since all the resources are reconciled during an execution and an execution can be triggered quite often, also retries
49+
of a reconciliation can happen naturally in operators, the implementation of a `Reconciler`
50+
needs to be idempotent. Luckily, since operators are usually managing already declarative resources, this is trivial to
51+
do in most cases.
1852

19-
## Why to Have Automated Retries?
53+
### Sync or Async Way of Resource Handling
2054

21-
## Managing State
55+
In an implementation of reconciliation there can be a point when reconciler needs to wait a non-insignificant amount of
56+
time while a resource gets up and running. For example, reconciler would do some additional step only if a Pod is ready
57+
to receive requests. This problem can be approached in two ways synchronously or asynchronously.
2258

23-
## Dependent Resources
59+
The async way is just return from the reconciler, if there are informers properly in place for the target resource,
60+
reconciliation will be triggered on change. During the reconciliation the pod can be read from the cache of the informer
61+
and a check on it's state can be conducted again. The benefit of this approach is that it will free up the thread, so it
62+
can be used to reconcile other resources.
2463

25-
### EventSources and Caching
64+
The sync way would be to periodically poll the cache of the informer for the pod's state, until the target state is
65+
reached. This would block the thread until the state is reached, which in some cases could take quite long.
66+
67+
## Why to Have Automated Retries?
68+
69+
Automatic retries are in place by default, it can be fine-tuned, but in general it's not advised to turn of automatic
70+
retries. One of the reasons is that issues like network error naturally happen and are usually solved by a retry.
71+
Another typical situation is for example when a dependent resource or the custom resource is updated, during the update
72+
usually there is optimistic version control in place. So if someone updated the resource during reconciliation, maybe
73+
using `kubectl` or another process, the update would fail on a conflict. A retry solves this problem simply by executing
74+
the reconciliation again.
75+
76+
## Managing State
2677

27-
### Why are Events Irrelevant?
78+
When managing only kubernetes resources an explicit state is not necessary about the resources. The state can be
79+
read/watched, also filtered using labels. Or just following some naming convention. However, when managing external
80+
resources, there can be a situation for example when the created resource can only be addressed by an ID generated when
81+
the resource was created. This ID needs to be stored, so on next reconciliation it could be used to addressing the
82+
resource. One place where it could go is the status sub-resource. On the other hand by definition status should be just
83+
the result of a reconciliation. Therefore, it's advised in general, to put such state into a separate resource usually a
84+
Kubernetes Secret or ConfigMap or a dedicated CustomResource, where the structure can be also validated.
2885

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ public synchronized void stop() {
180180

181181
public synchronized void add(Controller controller) {
182182
final var configuration = controller.getConfiguration();
183-
final var resourceTypeName = configuration.getResourceTypeName();
183+
final var resourceTypeName = ReconcilerUtils
184+
.getResourceTypeNameWithVersion(configuration.getResourceClass());
184185
final var existing = controllers.get(resourceTypeName);
185186
if (existing != null) {
186187
throw new OperatorException("Cannot register controller '" + configuration.getName()

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ public void setApiVersion(String s) {
4040
return Constants.NO_FINALIZER.equals(finalizer) || validator.isFinalizerValid(finalizer);
4141
}
4242

43-
public static String getResourceTypeName(Class<? extends HasMetadata> resourceClass) {
44-
// todo: use fabric8 method when 5.12 is released
45-
// return HasMetadata.getFullResourceName(resourceClass);
43+
public static String getResourceTypeNameWithVersion(Class<? extends HasMetadata> resourceClass) {
44+
final var version = HasMetadata.getVersion(resourceClass);
45+
return getResourceTypeName(resourceClass) + "/" + version;
46+
}
47+
48+
public static String getResourceTypeName(
49+
Class<? extends HasMetadata> resourceClass) {
4650
final var group = HasMetadata.getGroup(resourceClass);
4751
final var plural = HasMetadata.getPlural(resourceClass);
4852
return (group == null || group.isEmpty()) ? plural : plural + "." + group;

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
/**
88
* If the custom resource's status implements this interface, the observed generation will be
9-
* automatically handled. The last observed generation will be updated on status. In addition to
10-
* that, controller configuration will be checked if is set to be generation aware. If generation
11-
* aware config is turned off, this interface is ignored.
9+
* automatically handled. The last observed generation will be updated on status.
1210
* <p>
1311
* In order for this automatic handling to work the status object returned by
1412
* {@link CustomResource#getStatus()} should not be null.

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3+
import java.lang.reflect.ParameterizedType;
4+
import java.time.Duration;
35
import java.util.Collections;
46
import java.util.List;
7+
import java.util.Optional;
8+
import java.util.Set;
59

610
import io.fabric8.kubernetes.api.model.HasMetadata;
711
import io.javaoperatorsdk.operator.ReconcilerUtils;
@@ -55,4 +59,8 @@ default List<DependentResourceConfiguration> getDependentResources() {
5559
default DependentResourceControllerFactory<R> dependentFactory() {
5660
return new DependentResourceControllerFactory<>() {};
5761
}
62+
63+
default Optional<Duration> reconciliationMaxInterval() {
64+
return Optional.of(Duration.ofHours(10L));
65+
}
5866
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3+
import java.time.Duration;
34
import java.util.HashSet;
45
import java.util.List;
56
import java.util.Set;
@@ -16,6 +17,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
1617
private String labelSelector;
1718
private ResourceEventFilter<R> customResourcePredicate;
1819
private final ControllerConfiguration<R> original;
20+
private Duration reconciliationMaxInterval;
1921

2022
private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
2123
finalizer = original.getFinalizer();
@@ -24,7 +26,9 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
2426
retry = original.getRetryConfiguration();
2527
labelSelector = original.getLabelSelector();
2628
customResourcePredicate = original.getEventFilter();
29+
reconciliationMaxInterval = original.reconciliationMaxInterval().orElse(null);
2730
this.original = original;
31+
2832
}
2933

3034
public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) {
@@ -74,6 +78,12 @@ public ControllerConfigurationOverrider<R> withCustomResourcePredicate(
7478
return this;
7579
}
7680

81+
public ControllerConfigurationOverrider<R> withReconciliationMaxInterval(
82+
Duration reconciliationMaxInterval) {
83+
this.reconciliationMaxInterval = reconciliationMaxInterval;
84+
return this;
85+
}
86+
7787
public ControllerConfiguration<R> build() {
7888
return new DefaultControllerConfiguration<>(
7989
original.getAssociatedReconcilerClassName(),
@@ -86,6 +96,7 @@ public ControllerConfiguration<R> build() {
8696
labelSelector,
8797
customResourcePredicate,
8898
original.getResourceClass(),
99+
reconciliationMaxInterval,
89100
original.getConfigurationService(),
90101
original.getDependentResources());
91102
}

0 commit comments

Comments
 (0)