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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
httpclient-tests:
strategy:
matrix:
httpclient: [ 'vertx', 'jdk', 'jetty' ]
httpclient: [ 'vertx-5', 'jdk', 'jetty' ]
uses: ./.github/workflows/integration-tests.yml
with:
java-version: 25
Expand Down
2 changes: 1 addition & 1 deletion bootstrapper-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.6.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>bootstrapper</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion caffeine-bounded-cache-support/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.6.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>caffeine-bounded-cache-support</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,34 @@ samples [here](https://github.com/java-operator-sdk/java-operator-sdk/tree/main/
in [related integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource2.java)
.

### Adding Common Metadata to All Managed Resources (Desired State Aspects)

Operators often need to mark every resource they manage in a uniform way, for example with a
`app.kubernetes.io/managed-by` label, so that these resources can easily be identified, selected or
garbage-collected later on. Instead of repeating that logic in every `desired()` implementation, a
`DesiredStateAspect` can be registered once, at the operator level, and is then applied to the
desired state of every Kubernetes dependent resource managed by the operator:

```java
Operator operator = new Operator(overrider -> overrider
.withDesiredStateAspects(List.of(
(desired, dependentResource, context) -> desired.getMetadata().getLabels()
.put("app.kubernetes.io/managed-by", "my-operator"))));
```

Aspects are applied, in registration order, right after the desired state has been computed and
before the desired state is matched against the actual resource, created or updated. As a
consequence, the metadata added by an aspect is part of the desired state proper: if it is removed
from the actual resource, or if the aspect itself changes, the associated secondary resources are
updated accordingly on the next reconciliation.

Since the desired state is computed at most once per reconciliation and cached in the `Context`,
aspects are called at most once per dependent resource and reconciliation. They are only called for
dependent resources whose desired state is a `HasMetadata`, meaning that external (non-Kubernetes)
dependent resources are left untouched. Implementations are expected to modify the provided desired
state in place and need to be thread-safe as they can be called concurrently for different primary
resources.

## "Read-only" Dependent Resources vs. Event Source

See Integration test for a read-only
Expand Down
29 changes: 28 additions & 1 deletion docs/content/en/docs/documentation/eventing.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,30 @@ See
also [CaffeineBoundedItemStores](https://github.com/operator-framework/java-operator-sdk/blob/main/caffeine-bounded-cache-support/src/main/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedItemStores.java)
for more details.

### Removing the Namespace Index

Informers keep an index from namespace to the resources cached for it. JOSDK never reads that index,
only the ones registered explicitly through `IndexerResourceCache.addIndexers(..)`, so it can be
removed to save an entry per cached resource:

```java
@ControllerConfiguration(informer = @Informer(withoutNamespaceIndex = true))
public class MyReconciler implements Reconciler<MyCustomResource> { }
```

The same option is available on `InformerEventSourceConfiguration.Builder` for event sources, and on
`InformerConfiguration.Builder`.

This matters most for informers that cache a large number of resources, and in particular together
with a custom [item store](#bounded-caches-for-informers) that keeps only a reduced form of each
resource: the index is keyed independently of what the store does with the resource itself, so
shrinking what is cached does not shrink the index.

Note that this takes part in the informer pool identity described below: an event source that
removes the index does not share an informer with one that keeps it. If two event sources watch the
same resource type and only one of them sets the option, they end up with two informers, and two
caches of that resource type, which can cost far more memory than the index ever did.

### Sharing Informers Between Controllers (Informer Pool)

{{% alert title="Experimental" color="warning" %}}
Expand All @@ -374,7 +398,10 @@ Two event sources share an informer when their effective informer configuration
- the resource type (or the group/version/kind for generic resources),
- the watched namespace,
- the label, field and shard selectors,
- the configured [item store](#bounded-caches-for-informers).
- the configured [item store](#bounded-caches-for-informers),
- whether the [namespace index is removed](#removing-the-namespace-index): it cannot be present for
one event source and absent for another on one shared informer, so event sources that disagree on
it are backed by separate informers.

The `informerListLimit` is intentionally *not* part of this identity: if two otherwise-equivalent
event sources request a different list limit, the existing informer is reused (a warning is logged
Expand Down
19 changes: 19 additions & 0 deletions docs/content/en/docs/documentation/operations/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ See also
the [integration test](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace)
for this feature.

## Keeping the JVM Alive

The operator normally keeps the JVM running through the non-daemon threads of its thread pools. If
nothing else does, for example because all the threads of the operator are daemon threads as it is
the case with virtual threads, the JVM exits as soon as the `main` method returns after starting
the operator. Call `Operator.keepAlive()` to have a non-daemon thread keep the JVM running until the
operator is stopped:

```java
public static void main(String[] args) {
Operator operator = new Operator();
operator.register(new MyReconciler());
operator.installShutdownHook();
operator.start();
operator.keepAlive();
}
```

## DependentResource-level configuration

It is possible to define custom annotations to configure custom `DependentResource` implementations. In order to provide
Expand Down Expand Up @@ -385,6 +403,7 @@ josdk.controller.mycontroller.namespaces=team-a,team-b
| `josdk.controller.<name>.informer.shard-selector` | `String` | Shard selector for the primary resource informer (alias for `shard-selector`) |
| `josdk.controller.<name>.informer.list-limit` | `Long` | Page size for paginated informer list requests; omit for no pagination |
| `josdk.controller.<name>.informer.comparable-resource-versions` | `Boolean` | Whether the resource versions of the primary resource can be treated as integers and thus compared |
| `josdk.controller.<name>.informer.without-namespace-index` | `Boolean` | Removes the namespace index the informer maintains; defaults to `false`. See [Removing the Namespace Index](../eventing#removing-the-namespace-index) |

#### Retry

Expand Down
43 changes: 43 additions & 0 deletions docs/content/en/docs/migration/v5-7-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: Migrating from v5.6 to v5.7
description: Migrating from v5.6 to v5.7
---

## Fabric8 Kubernetes Client upgraded to v8.0.0

v5.7 uses version 8.0.0 of the
[Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client). Please consult the
client's own release notes and migration guide for the complete list of changes. The ones that are
most likely to affect operators built with JOSDK are described below.

### Jackson 3

The client moved from Jackson 2 to Jackson 3. The packages changed from `com.fasterxml.jackson` to
`tools.jackson` (the annotations from `jackson-annotations` are the exception and remain in
`com.fasterxml.jackson.annotation`). If your operator uses Jackson types directly, for example
`JsonNode` obtained from the client's `KubernetesSerialization`, update the imports and the
dependencies accordingly:

```java
// before
import com.fasterxml.jackson.databind.JsonNode;

// after
import tools.jackson.databind.JsonNode;
```

Other notable Jackson 3 differences:

- `JsonProcessingException` was replaced by `JacksonException`, which is unchecked.
- `JsonNode.fields()` was replaced by `properties()`, `fieldNames()` by `propertyNames()` and
`asText()` by `asString()`.
- `ObjectMapper` instances are created using builders, for example `YAMLMapper.builder().build()`.

### Ambiguous `withShardSelector(null)`

`Filterable` now has an additional `withShardSelector(ShardSelector)` overload next to
`withShardSelector(String)`. Calls passing a bare `null` no longer compile, cast the argument:

```java
client.pods().withShardSelector((String) null);
```
2 changes: 1 addition & 1 deletion micrometer-support/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.6.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>micrometer-support</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion migration/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.6.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>migration</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion operator-framework-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-bom</artifactId>
<version>5.6.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Operator SDK - Bill of Materials</name>
<description>Java SDK for implementing Kubernetes operators</description>
Expand Down
11 changes: 8 additions & 3 deletions operator-framework-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.6.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand All @@ -46,14 +46,19 @@
<exclusions>
<exclusion>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-httpclient-okhttp</artifactId>
<artifactId>kubernetes-httpclient-vertx-5</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-httpclient-${fabric8-httpclient-impl.name}</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
Expand Down Expand Up @@ -128,7 +133,7 @@
<version>${git-commit-id-maven-plugin.version}</version>
<configuration>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<generateGitPropertiesFilename>${project.build.outputDirectory}/version.properties</generateGitPropertiesFilename>
<generateGitPropertiesFilename>${project.build.outputDirectory}/operator-sdk-version.properties</generateGitPropertiesFilename>
<includeOnlyProperties>
<includeOnlyProperty>^git.build.time$</includeOnlyProperty>
<includeOnlyProperty>^git.commit.id.(abbrev|full)$</includeOnlyProperty>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer;

import org.slf4j.Logger;
Expand All @@ -45,6 +47,7 @@ public class Operator implements LifecycleAware {
private ConfigurationService configurationService;
private volatile boolean started = false;
private final AtomicBoolean shutdownHookInstalled = new AtomicBoolean(false);
private final AtomicReference<Thread> keepAliveThread = new AtomicReference<>();

public Operator() {
init(initConfigurationService(null, null), true);
Expand Down Expand Up @@ -175,6 +178,38 @@ public void installShutdownHook(Duration gracefulShutdownTimeout) {
installShutdownHook();
}

/**
* Keeps the JVM running until this operator is stopped, by starting a non-daemon thread that does
* nothing but wait. This is useful when the {@code main} method of the application returns after
* starting the operator and nothing else keeps the JVM alive, for example when the threads of the
* operator are all daemon threads, as is the case with virtual threads.
*
* <p>It can be called before or after {@link #start()}, and calling it again while the thread is
* already running has no effect. The thread ends when {@link #stop()} is called, so the operator
* can be stopped from a shutdown hook, see {@link #installShutdownHook()}.
*/
public void keepAlive() {
var thread =
new Thread(
() -> {
while (!Thread.currentThread().isInterrupted()) {
LockSupport.park();
}
},
"josdk-keep-alive");
thread.setDaemon(false);
if (keepAliveThread.compareAndSet(null, thread)) {
thread.start();
}
}

private void stopKeepAlive() {
var thread = keepAliveThread.getAndSet(null);
if (thread != null) {
thread.interrupt();
}
}

public KubernetesClient getKubernetesClient() {
return configurationService.getKubernetesClient();
}
Expand Down Expand Up @@ -240,6 +275,7 @@ public synchronized void start() {
*/
@Override
public void stop() throws OperatorException {
stopKeepAlive();
Duration reconciliationTerminationTimeout =
configurationService.reconciliationTerminationTimeout();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.javaoperatorsdk.operator.api.config;

import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
Expand All @@ -41,6 +42,7 @@
import io.javaoperatorsdk.operator.api.reconciler.Experimental;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceFactory;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DesiredStateAspect;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
Expand Down Expand Up @@ -145,11 +147,11 @@ public <R extends HasMetadata> R clone(R object) {
* you do not need to worry about setting the namespace information in most cases).
*
* <p>Previous versions of this class provided direct access to the serialization mechanism (via
* {@link com.fasterxml.jackson.databind.ObjectMapper}) or the client's configuration. This was
* somewhat confusing, in particular with respect to changes made in the Fabric8 client
* serialization architecture made in 6.7. The proper way to configure these aspects is now to
* configure the Kubernetes client accordingly and the SDK will extract the information it needs
* from this instance. The recommended way to do so is to create your operator with {@link
* {@link tools.jackson.databind.ObjectMapper}) or the client's configuration. This was somewhat
* confusing, in particular with respect to changes made in the Fabric8 client serialization
* architecture made in 6.7. The proper way to configure these aspects is now to configure the
* Kubernetes client accordingly and the SDK will extract the information it needs from this
* instance. The recommended way to do so is to create your operator with {@link
* io.javaoperatorsdk.operator.Operator#Operator(Consumer)}, passing your custom instance with
* {@link ConfigurationServiceOverrider#withKubernetesClient(KubernetesClient)}.
*
Expand Down Expand Up @@ -539,4 +541,18 @@ default InformerPool informerPool() {
pool.setConfigurationService(this);
return pool;
}

/**
* Retrieves the {@link DesiredStateAspect}s applied to the desired state of all the Kubernetes
* dependent resources managed by the operator. Aspects are applied in the order in which they are
* returned, right after the desired state has been computed, and are typically used to add common
* metadata (such as a label identifying the operator managing the resource) to all the resources
* the operator creates or updates.
*
* @return the list of aspects to apply to computed desired states, empty by default
* @since 5.6.0
*/
default List<DesiredStateAspect> desiredStateAspects() {
return List.of();
}
}
Loading
Loading