diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6441364993..26c3322fbd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 3f1353efea..d29d1f1d21 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -12,7 +12,7 @@ on: http-client: type: string required: false - default: 'vertx' + default: 'vertx-5' experimental: type: boolean required: false diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index 8f5a7c5684..b7fcff8ecc 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT bootstrapper diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml index d3d26a03fd..8266016d02 100644 --- a/caffeine-bounded-cache-support/pom.xml +++ b/caffeine-bounded-cache-support/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT caffeine-bounded-cache-support diff --git a/docs/content/en/docs/documentation/features.md b/docs/content/en/docs/documentation/features.md index 8cd109a5c4..e70f9f1154 100644 --- a/docs/content/en/docs/documentation/features.md +++ b/docs/content/en/docs/documentation/features.md @@ -27,21 +27,35 @@ public class DeploymentReconciler **Note:** This feature is provided by the [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client), not JOSDK itself. -To automatically generate CRD manifests from your annotated Custom Resource classes, add this dependency to your project: +To automatically generate CRD manifests from your annotated Custom Resource classes, configure the +Fabric8 CRD generator Maven plugin. Use the same Fabric8 version as the SDK: ```xml - - + io.fabric8 - crd-generator-apt - provided - + crd-generator-maven-plugin + ${fabric8-client.version} + + + + generate + + + + ``` -The CRD will be generated in `target/classes/META-INF/fabric8` (or `target/test-classes/META-INF/fabric8` for test scope) with the CRD name suffixed by the generated spec version. +Add the plugin under `build/plugins` and set `fabric8-client.version` to `8.0.0`. +The old `crd-generator-apt` annotation processor was removed in Fabric8 8. + +The CRD will be generated in `target/classes/META-INF/fabric8` with the filename suffixed by the +generated spec version. For test resources, configure `classesToScan` to +`${project.build.testOutputDirectory}` and `classpath` to `WITH_ALL_DEPENDENCIES_AND_TESTS`, and +bind the plugin execution to `process-test-classes`. -For example, a CR using the `java-operator-sdk.io` group with a `mycrs` plural form will result in these files: +For example, a CR using the `java-operator-sdk.io` group with a `mycrs` plural form will result in: - `mycrs.java-operator-sdk.io-v1.yml` -- `mycrs.java-operator-sdk.io-v1beta1.yml` + +Only `apiextensions.k8s.io/v1` CRDs are generated. **Note for Quarkus users:** If you're using the `quarkus-operator-sdk` extension, you don't need to add any extra dependency for CRD generation - the extension handles this automatically. diff --git a/docs/content/en/docs/migration/fabric8-8-migration.md b/docs/content/en/docs/migration/fabric8-8-migration.md new file mode 100644 index 0000000000..14ad0e9f18 --- /dev/null +++ b/docs/content/en/docs/migration/fabric8-8-migration.md @@ -0,0 +1,82 @@ +--- +title: Migrating from SDK 5 to SDK 6 +description: Updating operators for SDK 6, Fabric8 Kubernetes Client 8, and Jackson 3 +--- + +## Breaking changes in SDK 6 + +Java Operator SDK 6 introduces source and binary incompatibilities through its upgrade to +Fabric8 8 and Jackson 3. Upgrade the SDK dependencies and `operator-framework-bom` together, +migrate custom Jackson integrations, and recompile your operator. This is not a drop-in +replacement for SDK 5. + +## Fabric8 Kubernetes Client 8 + +The SDK now uses Fabric8 Kubernetes Client `8.0.0`, which requires Java 17 or newer, matching the +SDK's existing Java baseline. Align any explicit Fabric8 dependencies and the +`crd-generator-maven-plugin` with this version. + +See the [Fabric8 8 migration guide](https://github.com/fabric8io/kubernetes-client/blob/v8.0.0/doc/MIGRATION-v8.md) +for the complete list of client and CRD generator changes. + +## Jackson 3 + +The Kubernetes client, resource matchers, and YAML configuration provider now use Jackson 3. +Code that uses the client's Jackson types must migrate from `com.fasterxml.jackson.core`, +`com.fasterxml.jackson.databind`, and `com.fasterxml.jackson.dataformat` to the corresponding +`tools.jackson` packages. Maven dependencies use `tools.jackson.core:jackson-core`, +`tools.jackson.core:jackson-databind`, and `tools.jackson.dataformat:jackson-dataformat-yaml`. +Jackson annotations such as `@JsonProperty` remain in `com.fasterxml.jackson.annotation`, +provided by `com.fasterxml.jackson.core:jackson-annotations`. + +Custom serializers, deserializers, and databind annotations must use Jackson 3 types: +`ValueSerializer`, `ValueDeserializer`, and `tools.jackson.databind.annotation`. Jackson 2 +databind annotations are not used by the client, even when Jackson 2 is also on the classpath. +Java time support is built in, so remove `JavaTimeModule` registrations. + +Jackson 3 mappers are immutable. Use builders instead of `registerModule` or `configure`. +When supplying a custom mapper to the Kubernetes client, preserve the Fabric8 compatibility +defaults: + +```java +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; +import io.fabric8.kubernetes.model.jackson.Jackson2JdkTypesModule; +import tools.jackson.databind.json.JsonMapper; + +var mapper = JsonMapper.builderWithJackson2Defaults() + .addModule(new Jackson2JdkTypesModule()) + .build(); +var client = new KubernetesClientBuilder() + .withKubernetesSerialization(new KubernetesSerialization(mapper, true)) + .build(); +``` + +The default client already applies these compatibility settings. Supplying a mapper configures +a copy; the original mapper is not modified. See the Fabric8 migration guide for wire-format +differences that cannot be restored, and review regenerated CRDs before applying them. + +## HTTP client selection + +The `operator-framework` module continues to use the JDK HTTP client by default. Fabric8's +default HTTP client is now `kubernetes-httpclient-vertx-5`, which the core module excludes so +that it does not bring a second HTTP implementation into operators. + +If you select Vert.x through `fabric8-httpclient-impl.name`, use `vertx-5` instead of `vertx`. +Explicit Vert.x dependencies should use `io.fabric8:kubernetes-httpclient-vertx-5` and compatible +Vert.x 5 dependencies. Applications depending only on `operator-framework-core` must provide +their chosen Fabric8 HTTP client implementation. + +Vert.x 5 requires Netty 4.2. Check application and platform BOMs for dependencies pinned to +Vert.x 4 or Netty 4.1 before selecting this client; incompatible versions can prevent client +startup. See the Fabric8 migration guide if you need to remain on Vert.x 4. + +The Jetty HTTP client now uses Jetty 12.1 and cannot share a classpath with Jetty 9, 10, or 11. +Upgrade any application dependencies using older Jetty versions or choose a different +Fabric8 HTTP client implementation. + +## CRD generation + +Fabric8 removed the `crd-generator-apt` annotation processor. Use the +[`crd-generator-maven-plugin`](../../documentation/features/#automatic-crd-generation) instead. +Only `apiextensions.k8s.io/v1` CRDs are generated. diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index f79444b248..ca29408df1 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT micrometer-support @@ -64,7 +64,7 @@ io.fabric8 - kubernetes-httpclient-vertx + kubernetes-httpclient-vertx-5 test diff --git a/migration/pom.xml b/migration/pom.xml index 56a1bd82f0..40fca48edb 100644 --- a/migration/pom.xml +++ b/migration/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT migration diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 2cd8d16139..8e10ec61dd 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk operator-framework-bom - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT pom Operator SDK - Bill of Materials Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 6f5d63cc53..246886de0b 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT ../pom.xml @@ -46,10 +46,15 @@ io.fabric8 - kubernetes-httpclient-okhttp + kubernetes-httpclient-vertx-5 + + io.fabric8 + kubernetes-httpclient-${fabric8-httpclient-impl.name} + test + org.slf4j slf4j-api 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 db1b9a5fa5..c86a1f342a 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 @@ -49,6 +49,8 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.pool.DefaultInformerPool; import io.javaoperatorsdk.operator.processing.event.source.informer.pool.InformerPool; +import tools.jackson.databind.ObjectMapper; + /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { @@ -145,11 +147,11 @@ public R clone(R object) { * you do not need to worry about setting the namespace information in most cases). * *

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 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)}. * diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 23fb29151f..84be53d722 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -25,7 +25,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.Matcher; -import com.fasterxml.jackson.databind.JsonNode; +import tools.jackson.databind.JsonNode; public class GenericKubernetesResourceMatcher { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/matcher/MatcherUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/matcher/MatcherUtils.java index 413a1c4acc..9bd6994fb2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/matcher/MatcherUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/matcher/MatcherUtils.java @@ -20,10 +20,10 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.matcher.Matcher; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.NullNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.NullNode; +import tools.jackson.databind.node.ObjectNode; /** Shared helpers for the client-side patch based {@link Matcher} implementations. */ final class MatcherUtils { @@ -66,9 +66,7 @@ private static JsonNode applyMergePatch(JsonNode target, JsonNode patch) { } var targetObject = target.isObject() ? (ObjectNode) target : JsonNodeFactory.instance.objectNode(); - var fields = patch.fields(); - while (fields.hasNext()) { - var entry = fields.next(); + for (var entry : patch.properties()) { if (entry.getValue().isNull()) { targetObject.remove(entry.getKey()); } else { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/Jackson3SerializationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/Jackson3SerializationTest.java new file mode 100644 index 0000000000..ab9abeaa9f --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/Jackson3SerializationTest.java @@ -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.api.config; + +import java.time.Duration; +import java.util.Locale; +import java.util.Objects; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; +import io.fabric8.kubernetes.model.jackson.Jackson2JdkTypesModule; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.matcher.JsonMergePatchMatcher; +import io.javaoperatorsdk.operator.processing.matcher.JsonPatchMatcher; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.cfg.ContextAttributes; +import tools.jackson.databind.json.JsonMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class Jackson3SerializationTest { + + private static final String PREFIX_ATTRIBUTE = "valuePrefix"; + private static final String PREFIX = "custom:"; + + private KubernetesClient client; + private ConfigurationService configuration; + + @BeforeEach + void setup() { + var mapper = + JsonMapper.builderWithJackson2Defaults() + .addModule(new Jackson2JdkTypesModule()) + .defaultAttributes( + ContextAttributes.getEmpty().withSharedAttribute(PREFIX_ATTRIBUTE, PREFIX)) + .build(); + client = + new KubernetesClientBuilder() + .withKubernetesSerialization(new KubernetesSerialization(mapper, true)) + .build(); + configuration = + new ConfigurationServiceOverrider(new BaseConfigurationService()) + .withKubernetesClient(client) + .build(); + } + + @AfterEach + void cleanup() { + client.close(); + } + + @Test + void configuredClientClonesCustomResourceUsingJackson3Annotations() { + var original = resource("value"); + var node = + configuration + .getKubernetesClient() + .getKubernetesSerialization() + .convertValue(original, JsonNode.class); + + assertThat(configuration.getKubernetesClient()).isSameAs(client); + assertThat(node.at("/spec/value").asString()).isEqualTo("custom:value"); + assertThat(node.at("/spec/timeout").asString()).isEqualTo("PT30S"); + + var cloned = configuration.getResourceCloner().clone(original); + assertThat(cloned).isNotSameAs(original); + assertThat(cloned.getSpec()).isNotSameAs(original.getSpec()).isEqualTo(original.getSpec()); + assertThat(cloned.getMetadata()).isNotSameAs(original.getMetadata()); + assertThat(cloned.getMetadata().getName()).isEqualTo(original.getMetadata().getName()); + } + + @Test + void matchersUseConfiguredSerializationRatherThanPojoEquality() { + Context context = mock(); + when(context.getClient()).thenReturn(configuration.getKubernetesClient()); + var desired = resource("VALUE"); + var actual = configuration.getResourceCloner().clone(desired); + + assertThat(desired.getSpec()).isNotEqualTo(actual.getSpec()); + assertThat(JsonPatchMatcher.getInstance().matches(desired, actual, context)).isTrue(); + assertThat(JsonMergePatchMatcher.getInstance().matches(desired, actual, context)).isTrue(); + + var changed = resource("different"); + assertThat(JsonPatchMatcher.getInstance().matches(changed, actual, context)).isFalse(); + assertThat(JsonMergePatchMatcher.getInstance().matches(changed, actual, context)).isFalse(); + } + + private static SerializationCustomResource resource(String value) { + var resource = new SerializationCustomResource(); + resource.getMetadata().setName("serialization-test"); + resource.setSpec(new SerializationSpec(new EncodedValue(value), Duration.ofSeconds(30))); + return resource; + } + + @Group("serialization.javaoperatorsdk.io") + @Version("v1") + public static class SerializationCustomResource extends CustomResource {} + + public record SerializationSpec(EncodedValue value, Duration timeout) {} + + @JsonSerialize(using = EncodedValueSerializer.class) + @JsonDeserialize(using = EncodedValueDeserializer.class) + public record EncodedValue(String value) {} + + public static class EncodedValueSerializer extends ValueSerializer { + @Override + public void serialize( + EncodedValue value, JsonGenerator generator, SerializationContext context) { + var prefix = (String) Objects.requireNonNull(context.getAttribute(PREFIX_ATTRIBUTE)); + generator.writeString(prefix + value.value().toLowerCase(Locale.ROOT)); + } + } + + public static class EncodedValueDeserializer extends ValueDeserializer { + @Override + public EncodedValue deserialize(JsonParser parser, DeserializationContext context) { + var prefix = (String) Objects.requireNonNull(context.getAttribute(PREFIX_ATTRIBUTE)); + return new EncodedValue(parser.getString().substring(prefix.length())); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourceNamespaceChangeTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourceNamespaceChangeTest.java index 7a2ee4ee16..e28becd6f2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourceNamespaceChangeTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourceNamespaceChangeTest.java @@ -161,7 +161,7 @@ private SharedIndexInformer informer() { .resources(Deployment.class) .inNamespace("ns1") .withLabelSelector((String) null) - .withShardSelector(null) + .withShardSelector((String) null) .runnableInformer(0); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourcePoolIdentityTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourcePoolIdentityTest.java index a2d0759fbc..5ee911de1f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourcePoolIdentityTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourcePoolIdentityTest.java @@ -70,7 +70,7 @@ void setup() { .resources(Deployment.class) .inNamespace(NAMESPACE) .withLabelSelector((String) null) - .withShardSelector(null) + .withShardSelector((String) null) .runnableInformer(0); when(informer.getStore()).thenReturn(mock(Cache.class)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourceStartFailureTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourceStartFailureTest.java index a4d78ace74..17ff0f38b6 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourceStartFailureTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSourceStartFailureTest.java @@ -88,7 +88,7 @@ private void stubInformerStore() { .resources(Deployment.class) .inNamespace("ns1") .withLabelSelector((String) null) - .withShardSelector(null) + .withShardSelector((String) null) .runnableInformer(0); when(informer.getStore()).thenReturn(mock(Cache.class)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/matcher/MatcherUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/matcher/MatcherUtilsTest.java new file mode 100644 index 0000000000..7998557f70 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/matcher/MatcherUtilsTest.java @@ -0,0 +1,129 @@ +/* + * 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.processing.matcher; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.json.JsonMapper; + +import static org.assertj.core.api.Assertions.assertThat; + +class MatcherUtilsTest { + + private static final JsonMapper MAPPER = JsonMapper.builder().build(); + + @Test + void mergePatchMatchesNestedValuesWithoutChangingActual() { + var actual = + MAPPER.readTree( + """ + {"spec":{"replicas":1,"extra":"value"}} + """); + var desired = + MAPPER.readTree( + """ + {"spec":{"replicas":1}} + """); + var original = actual.deepCopy(); + + assertThat(MatcherUtils.mergePatchMatches(actual, desired)).isTrue(); + assertThat(actual).isEqualTo(original); + } + + @Test + void mergePatchDoesNotMatchWhenNullRemovesExistingField() { + var actual = + MAPPER.readTree( + """ + {"spec":{"replicas":1}} + """); + var desired = + MAPPER.readTree( + """ + {"spec":{"replicas":null}} + """); + var original = actual.deepCopy(); + + assertThat(MatcherUtils.mergePatchMatches(actual, desired)).isFalse(); + assertThat(actual).isEqualTo(original); + } + + @Test + void mergePatchMatchesWhenNullTargetsAbsentField() { + var actual = + MAPPER.readTree( + """ + {"spec":{}} + """); + var desired = + MAPPER.readTree( + """ + {"spec":{"replicas":null}} + """); + + assertThat(MatcherUtils.mergePatchMatches(actual, desired)).isTrue(); + } + + @Test + void mergePatchReplacesScalarWithObject() { + var actual = + MAPPER.readTree( + """ + {"spec":"value"} + """); + var desired = + MAPPER.readTree( + """ + {"spec":{"replicas":1}} + """); + + assertThat(MatcherUtils.mergePatchMatches(actual, desired)).isFalse(); + } + + @Test + void mergePatchReplacesArraysRatherThanMergingThem() { + var actual = + MAPPER.readTree( + """ + {"values":[1,2]} + """); + var desired = + MAPPER.readTree( + """ + {"values":[1]} + """); + + assertThat(MatcherUtils.mergePatchMatches(actual, actual)).isTrue(); + assertThat(MatcherUtils.mergePatchMatches(actual, desired)).isFalse(); + } + + @Test + void jsonPatchUsesJackson3Nodes() { + var actual = + MAPPER.readTree( + """ + {"spec":{"replicas":1}} + """); + var desired = + MAPPER.readTree( + """ + {"spec":{"replicas":2}} + """); + + assertThat(MatcherUtils.jsonPatchMatches(actual, actual.deepCopy())).isTrue(); + assertThat(MatcherUtils.jsonPatchMatches(actual, desired)).isFalse(); + } +} diff --git a/operator-framework-junit/pom.xml b/operator-framework-junit/pom.xml index a46d4f6512..91e87055dd 100644 --- a/operator-framework-junit/pom.xml +++ b/operator-framework-junit/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT operator-framework-junit diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index c0e8952668..9e91485637 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT operator-framework diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java index fc80e46a43..396d72e587 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java @@ -16,7 +16,6 @@ package io.javaoperatorsdk.operator.config.loader.provider; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; @@ -28,8 +27,8 @@ import io.javaoperatorsdk.operator.config.loader.ConfigProvider; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.dataformat.yaml.YAMLMapper; /** * A {@link ConfigProvider} that resolves configuration values from a YAML file. @@ -44,7 +43,7 @@ public class YamlConfigProvider implements ConfigProvider { private static final Logger log = LoggerFactory.getLogger(YamlConfigProvider.class); - private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); + private static final YAMLMapper MAPPER = YAMLMapper.builder().build(); private final Map data; @@ -100,8 +99,8 @@ private static Map load(Path path) { return Map.of(); } - try (InputStream in = Files.newInputStream(path); - com.fasterxml.jackson.core.JsonParser parser = MAPPER.getFactory().createParser(in)) { + try (var in = Files.newInputStream(path); + var parser = MAPPER.createParser(in)) { if (parser.nextToken() == null) { log.debug( "YAML file contains no mappings (possibly empty or comments only). " @@ -111,6 +110,8 @@ private static Map load(Path path) { } Map result = MAPPER.readValue(parser, Map.class); return result != null ? result : Map.of(); + } catch (JacksonException e) { + throw new UncheckedIOException("Failed to load config YAML from " + path, new IOException(e)); } catch (IOException e) { throw new UncheckedIOException("Failed to load config YAML from " + path, e); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiversioncrd/MultiVersionCRDIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiversioncrd/MultiVersionCRDIT.java index b2606ad740..c6dee7195d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiversioncrd/MultiVersionCRDIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiversioncrd/MultiVersionCRDIT.java @@ -32,7 +32,7 @@ import io.javaoperatorsdk.operator.api.config.InformerStoppedHandler; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -104,7 +104,7 @@ public void onStop(SharedIndexInformer informer, Throwable ex) { .get(0) .get("apiVersion") .asText()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new RuntimeException(e); } }); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java index 328ac6d83f..3ca569a70a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java @@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import tools.jackson.core.JacksonException; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -173,7 +175,55 @@ void throwsForMalformedYaml(@TempDir Path dir) throws IOException { - item2 """); assertThatExceptionOfType(UncheckedIOException.class) - .isThrownBy(() -> new YamlConfigProvider(file)); + .isThrownBy(() -> new YamlConfigProvider(file)) + .withMessageContaining(file.toString()) + .withCauseInstanceOf(IOException.class) + .havingCause() + .withCauseInstanceOf(JacksonException.class); + } + + @Test + void throwsForInvalidYamlSyntax(@TempDir Path dir) throws IOException { + var file = dir.resolve("invalid.yaml"); + Files.writeString(file, "key: [unterminated"); + + assertThatExceptionOfType(UncheckedIOException.class) + .isThrownBy(() -> new YamlConfigProvider(file)) + .withMessageContaining(file.toString()) + .withCauseInstanceOf(IOException.class) + .havingCause() + .withCauseInstanceOf(JacksonException.class); + } + + @Test + void nullDocumentReturnsEmpty(@TempDir Path dir) throws IOException { + var file = dir.resolve("null.yaml"); + Files.writeString(file, "null"); + + var provider = new YamlConfigProvider(file); + assertThat(provider.getValue("any.key", String.class)).isEmpty(); + } + + @Test + void loadsTypedValuesFromFile(@TempDir Path dir) throws IOException { + var file = dir.resolve("typed.yaml"); + Files.writeString( + file, + """ + josdk: + enabled: true + limit: 2147483648 + ratio: 3.14 + timeout: PT30S + unset: null + """); + + var provider = new YamlConfigProvider(file); + assertThat(provider.getValue("josdk.enabled", Boolean.class)).hasValue(true); + assertThat(provider.getValue("josdk.limit", Long.class)).hasValue(2147483648L); + assertThat(provider.getValue("josdk.ratio", Double.class)).hasValue(3.14); + assertThat(provider.getValue("josdk.timeout", Duration.class)).hasValue(Duration.ofSeconds(30)); + assertThat(provider.getValue("josdk.unset", String.class)).isEmpty(); } @Test diff --git a/pom.xml b/pom.xml index e9277efe34..5cab963f5d 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT pom Operator SDK for Java Java SDK for implementing Kubernetes operators @@ -71,7 +71,7 @@ https://sonarcloud.io jdk 6.1.3 - 7.9.0 + 8.0.0 2.0.20 2.26.1 5.24.0 @@ -233,7 +233,7 @@ io.fabric8 - kubernetes-httpclient-vertx + kubernetes-httpclient-vertx-5 ${fabric8-client.version} diff --git a/sample-operators/controller-namespace-deletion/pom.xml b/sample-operators/controller-namespace-deletion/pom.xml index cb685c82df..46ec61d63b 100644 --- a/sample-operators/controller-namespace-deletion/pom.xml +++ b/sample-operators/controller-namespace-deletion/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT sample-controller-namespace-deletion diff --git a/sample-operators/kotlin-operator/pom.xml b/sample-operators/kotlin-operator/pom.xml index 62057fbbfc..730aaab7f3 100644 --- a/sample-operators/kotlin-operator/pom.xml +++ b/sample-operators/kotlin-operator/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT sample-kotlin-operator diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index a07c12034a..5917f3e290 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 5f5cf70b4c..f4ce0eb7ba 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/operations/pom.xml b/sample-operators/operations/pom.xml index 17ad0db462..57318eba92 100644 --- a/sample-operators/operations/pom.xml +++ b/sample-operators/operations/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT sample-operations diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index e658955aef..1d99df33a7 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/README.md b/sample-operators/tomcat-operator/README.md index 82d633d237..11781c616c 100644 --- a/sample-operators/tomcat-operator/README.md +++ b/sample-operators/tomcat-operator/README.md @@ -48,8 +48,8 @@ Before you run it you have to install the CRDs on your cluster by running: - `kubectl apply -f target/classes/META-INF/fabric8/tomcats.tomcatoperator.io-v1.yml` - `kubectl apply -f target/classes/META-INF/fabric8/webapps.tomcatoperator.io-v1.yml` -The CRDs are generated automatically from your code by simply adding the `crd-generator-apt` -dependency to your `pom.xml` file. +The CRDs are generated automatically from your code by the `crd-generator-maven-plugin` +configured in `pom.xml`. When the Operator is running you can create some Tomcat Custom Resources. You can find a sample custom resources in the k8s folder. @@ -81,4 +81,3 @@ Deployment so the InformerEventSource can watch the right Deployments. The Tomca to set `ownerReference` on the Deployment so later the InformerEventSource can identify which Tomcat does the Deployment belong to. This is necessary so the framework can call the Controller `reconcile` method correctly. - diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index dc6c95c662..7416bee622 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT sample-tomcat-operator @@ -49,13 +49,13 @@ io.fabric8 - kubernetes-httpclient-okhttp + kubernetes-httpclient-${fabric8-httpclient-impl.name} io.fabric8 - kubernetes-httpclient-vertx + kubernetes-httpclient-vertx-5 org.apache.logging.log4j diff --git a/sample-operators/webpage/README.md b/sample-operators/webpage/README.md index 96329d18a9..0e42d134b6 100644 --- a/sample-operators/webpage/README.md +++ b/sample-operators/webpage/README.md @@ -41,8 +41,8 @@ Kubernetes cluster. When you start it, it will use the current kubectl context o Before you run it you have to install the CRD on your cluster by running `kubectl apply -f target/classes/META-INF/fabric8/webpages.sample.javaoperatorsdk-v1.yml`. -The CRD is generated automatically from your code by simply adding the `crd-generator-apt` -dependency to your `pom.xml` file. +The CRD is generated automatically from your code by the `crd-generator-maven-plugin` +configured in `pom.xml`. When the Operator is running you can create some Webserver Custom Resources. You can find a sample custom resource in `k8s/webpage.yaml`. You can create it by running `kubectl apply -f k8s/webpage.yaml` diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 734c5b7f6a..da377d7dcd 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT sample-webpage-operator diff --git a/test-index-processor/pom.xml b/test-index-processor/pom.xml index 58cec3a8e7..f6a8e8c981 100644 --- a/test-index-processor/pom.xml +++ b/test-index-processor/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.6.4-SNAPSHOT + 6.0.0-SNAPSHOT test-index-processor