Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fbce8b2
chore: set next version to 999-SNAPSHOT (#3469)
csviri Jul 3, 2026
7e5a900
improve: integration test to showcase external resource state in stat…
csviri Jul 29, 2026
3fecf20
Informer pools (#3325)
csviri Aug 1, 2026
873eca1
chore(deps): bump org.apache.maven.plugins:maven-jar-plugin (#3511)
dependabot[bot] Jul 23, 2026
0ddecbd
chore(deps): bump openrewrite.version from 8.87.5 to 8.87.7 (#3510)
dependabot[bot] Jul 23, 2026
eb7958d
chore(deps): bump com.diffplug.spotless:spotless-maven-plugin from 3.…
dependabot[bot] Jul 28, 2026
fffe39c
fix: use explicit JSON merge patch type in merge patch operations (#3…
csviri Jul 28, 2026
9e5927a
test: verify Kotlin interop with a unit test and a Kotlin sample oper…
hej090224 Jul 29, 2026
8ad1115
chore(deps): bump openrewrite.version from 8.87.7 to 8.88.0 (#3516)
dependabot[bot] Jul 30, 2026
14912ba
docs: facelift of operator list in README.md (#3534)
csviri Aug 1, 2026
510e173
fix: new kotlin sample parent version
csviri Aug 1, 2026
907fe61
fix: set configuration service for default pool (#3535)
csviri Aug 3, 2026
9272c99
perf: size the workflow result map from Workflow#size (#3547)
csviri Aug 11, 2026
aa809d4
refactor: drop the unused Options parameter of desiredForJsonPatch (#…
csviri Aug 11, 2026
db9a0fa
refactor: call existing helpers instead of re-implementing them (#3544)
csviri Aug 11, 2026
592bad3
test: fix flaky finalizer removal in TriggerReconcilerOnAllEventIT (#…
csviri Aug 11, 2026
d4bd3da
feat: detect dependent resource API version changes (#3536)
hej090224 Aug 11, 2026
b5022e2
refactor: resolve the informer target client without a downcast (#3548)
csviri Aug 11, 2026
484727d
refactor: clean up the Kubernetes resource matchers (#3546)
csviri Aug 11, 2026
51490a2
improve: followup PR for Informer Pools (#3541)
csviri Aug 12, 2026
9010df1
refactor: let ResourceState own the trigger-on-all-events flag (#3549)
csviri Aug 13, 2026
eba57e1
perf: avoid redundant work on informer event paths (#3545)
csviri Aug 17, 2026
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 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.5.1-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.5.1-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>caffeine-bounded-cache-support</artifactId>
Expand Down
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.5.1-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.5.1-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.5.1-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
2 changes: 1 addition & 1 deletion 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.5.1-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion operator-framework-junit/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.5.1-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>operator-framework-junit</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion operator-framework/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.5.1-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>operator-framework</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Java Operator SDK Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.javaoperatorsdk.operator.baseapi.externalstateinstatus;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.ShortNames;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("sample.javaoperatorsdk")
@Version("v1")
@ShortNames("essis")
public class ExternalStateInStatusCustomResource
extends CustomResource<ExternalStateInStatusSpec, ExternalStateInStatusStatus>
implements Namespaced {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright Java Operator SDK Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.javaoperatorsdk.operator.baseapi.externalstateinstatus;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.annotation.Sample;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
import io.javaoperatorsdk.operator.support.ExternalIDGenServiceMock;
import io.javaoperatorsdk.operator.support.ExternalServiceResetExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

@Sample(
tldr = "Managing an External Resource with State Stored in the Status",
description =
"""
Demonstrates how to manage an external resource (outside of Kubernetes) while storing its \
state - the generated external ID - in the status of the custom resource. The reconciler \
persists the ID with a status patch and relies on the stronger read-after-write \
consistency for updates so that the next reconciliation observes the stored ID and never \
creates a duplicate external resource. A fake external service stands in for the managed \
external system.
""")
@ExtendWith(ExternalServiceResetExtension.class)
class ExternalStateInStatusIT {

private static final String TEST_RESOURCE_NAME = "test1";

public static final String INITIAL_TEST_DATA = "initialTestData";
public static final String UPDATED_DATA = "updatedData";

private final ExternalIDGenServiceMock externalService = ExternalIDGenServiceMock.getInstance();

@RegisterExtension
LocallyRunOperatorExtension operator =
LocallyRunOperatorExtension.builder()
.withReconciler(ExternalStateInStatusReconciler.class)
.build();

@Test
void reconcilesResourceWithStateStoredInStatus() {
var resource = operator.create(testResource());
assertResourceCreated(INITIAL_TEST_DATA);

resource.getSpec().setData(UPDATED_DATA);
operator.replace(resource);
assertResourceCreated(UPDATED_DATA);

operator.delete(resource);
assertResourceDeleted();
}

private void assertResourceCreated(String expectedData) {
await()
.untilAsserted(
() -> {
var resources = externalService.listResources();
// exactly one external resource is created, no duplicates
assertThat(resources).hasSize(1);
var extRes = resources.get(0);
assertThat(extRes.getData()).isEqualTo(expectedData);

var cr = operator.get(ExternalStateInStatusCustomResource.class, TEST_RESOURCE_NAME);
assertThat(cr.getStatus()).isNotNull();
// the external resource state (its ID) is stored in the status
assertThat(cr.getStatus().getId()).isEqualTo(extRes.getId());
});
}

private void assertResourceDeleted() {
await().untilAsserted(() -> assertThat(externalService.listResources()).isEmpty());
}

private ExternalStateInStatusCustomResource testResource() {
var res = new ExternalStateInStatusCustomResource();
res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build());
res.setSpec(new ExternalStateInStatusSpec().setData(INITIAL_TEST_DATA));
return res;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright Java Operator SDK Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.javaoperatorsdk.operator.baseapi.externalstateinstatus;

import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingConfigurationBuilder;
import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource;
import io.javaoperatorsdk.operator.support.ExternalIDGenServiceMock;
import io.javaoperatorsdk.operator.support.ExternalResource;
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;

/**
* Manages an external resource (living in the {@link ExternalIDGenServiceMock fake external
* service}) while storing the external resource's state - here just its generated ID - directly in
* the <b>status</b> of the custom resource.
*
* <p>This pattern only works reliably thanks to the stronger read-after-write consistency for
* updates: after the external resource is created, its ID is persisted through {@link
* UpdateControl#patchStatus(io.fabric8.kubernetes.api.model.HasMetadata)}. The patched resource
* (holding the ID) is placed into the controller's cache, so the very next reconciliation observes
* the ID and does not create a duplicate external resource - even before the informer delivers the
* update event.
*/
@ControllerConfiguration
public class ExternalStateInStatusReconciler
implements Reconciler<ExternalStateInStatusCustomResource>,
Cleaner<ExternalStateInStatusCustomResource>,
TestExecutionInfoProvider {

private final AtomicInteger numberOfExecutions = new AtomicInteger(0);

private final ExternalIDGenServiceMock externalService = ExternalIDGenServiceMock.getInstance();

PerResourcePollingEventSource<ExternalResource, ExternalStateInStatusCustomResource, String>
externalResourceEventSource;

@Override
public UpdateControl<ExternalStateInStatusCustomResource> reconcile(
ExternalStateInStatusCustomResource resource,
Context<ExternalStateInStatusCustomResource> context) {
numberOfExecutions.addAndGet(1);

var externalResource = context.getSecondaryResource(ExternalResource.class);
if (externalResource.isEmpty()) {
var id = idFromStatus(resource);
if (id == null || externalService.read(id).isEmpty()) {
return createExternalResource(resource);
}
return UpdateControl.noUpdate();
}

var currentExternalResource = externalResource.orElseThrow();
if (!currentExternalResource.getData().equals(resource.getSpec().getData())) {
updateExternalResource(resource, currentExternalResource);
}
return UpdateControl.noUpdate();
}

private UpdateControl<ExternalStateInStatusCustomResource> createExternalResource(
ExternalStateInStatusCustomResource resource) {
var createdResource =
externalService.create(new ExternalResource(resource.getSpec().getData()));

// Make sure the freshly created external resource is available in the poll cache for the next
// reconciliation, so it is not created again.
externalResourceEventSource.handleRecentResourceCreate(
ResourceID.fromResource(resource), createdResource);

resource.setStatus(new ExternalStateInStatusStatus().setId(createdResource.getId()));
return UpdateControl.patchStatus(resource);
}

private void updateExternalResource(
ExternalStateInStatusCustomResource resource, ExternalResource externalResource) {
var newResource = new ExternalResource(externalResource.getId(), resource.getSpec().getData());
externalService.update(newResource);
externalResourceEventSource.handleRecentResourceUpdate(
ResourceID.fromResource(resource), newResource, externalResource);
}

@Override
public DeleteControl cleanup(
ExternalStateInStatusCustomResource resource,
Context<ExternalStateInStatusCustomResource> context) {
var id = idFromStatus(resource);
if (id != null) {
externalService.delete(id);
}
return DeleteControl.defaultDelete();
}

@Override
public int getNumberOfExecutions() {
return numberOfExecutions.get();
}

@Override
public List<EventSource<?, ExternalStateInStatusCustomResource>> prepareEventSources(
EventSourceContext<ExternalStateInStatusCustomResource> context) {

final PerResourcePollingEventSource.ResourceFetcher<
ExternalResource, ExternalStateInStatusCustomResource>
fetcher =
(ExternalStateInStatusCustomResource primaryResource) -> {
var id = idFromStatus(primaryResource);
if (id == null) {
return Collections.emptySet();
}
return externalService.read(id).map(Set::of).orElseGet(Collections::emptySet);
};
externalResourceEventSource =
new PerResourcePollingEventSource<>(
ExternalResource.class,
context,
new PerResourcePollingConfigurationBuilder<
ExternalResource, ExternalStateInStatusCustomResource, String>(
fetcher, Duration.ofMillis(300L))
.build());

return List.of(externalResourceEventSource);
}

private static String idFromStatus(ExternalStateInStatusCustomResource resource) {
return resource.getStatus() == null ? null : resource.getStatus().getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Java Operator SDK Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.javaoperatorsdk.operator.baseapi.externalstateinstatus;

public class ExternalStateInStatusSpec {

private String data;

public String getData() {
return data;
}

public ExternalStateInStatusSpec setData(String data) {
this.data = data;
return this;
}
}
Loading
Loading