diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 34bc6c7d0f..3fa5655cbc 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -22,7 +22,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: 25 + java-version: 17 cache: 'maven' - name: Check code format run: | diff --git a/docs/content/en/blog/releases/v5-3-release.md b/docs/content/en/blog/releases/v5-3-release.md index 512df03a84..12b9bfd30e 100644 --- a/docs/content/en/blog/releases/v5-3-release.md +++ b/docs/content/en/blog/releases/v5-3-release.md @@ -220,6 +220,11 @@ checks. `reconciliationFinished(..)` is extended with `RetryInfo`. `monitorSizeOf(..)` is removed. +### `ResourceAction` relocated + +`ResourceAction` in `io.javaoperatorsdk.operator.processing.event.source.controller` has been +removed. Use `io.javaoperatorsdk.operator.processing.event.source.ResourceAction` instead. + See the full [migration guide](/docs/migration/v5-3-migration) for details. ## Getting Started diff --git a/docs/content/en/docs/migration/v5-3-migration.md b/docs/content/en/docs/migration/v5-3-migration.md index e3d7c34a9d..08d6bca723 100644 --- a/docs/content/en/docs/migration/v5-3-migration.md +++ b/docs/content/en/docs/migration/v5-3-migration.md @@ -3,6 +3,30 @@ title: Migrating from v5.2 to v5.3 description: Migrating from v5.2 to v5.3 --- +## Automated Migration with OpenRewrite + +You can automatically apply all the migration changes described below using [OpenRewrite](https://docs.openrewrite.org/). +Add the following to your `pom.xml` and run `mvn rewrite:run`: + +```xml + + org.openrewrite.maven + rewrite-maven-plugin + 6.33.0 + + + io.javaoperatorsdk.operator.migration.V5_3Migration + + + + + io.javaoperatorsdk + migration + 5.3.1 + + + +``` ## Rename of JUnit module @@ -48,4 +72,10 @@ The following table shows the relevant method renames: Other changes: - `reconciliationFinished(..)` method is extended with `RetryInfo` -- `monitorSizeOf(..)` method is removed. \ No newline at end of file +- `monitorSizeOf(..)` method is removed. + +## ResourceAction relocation + +The `ResourceAction` enum has been removed from +`io.javaoperatorsdk.operator.processing.event.source.controller` use the one in package +`io.javaoperatorsdk.operator.processing.event.source.ResourceAction`; thus update your imports accordingly. \ No newline at end of file diff --git a/migration/pom.xml b/migration/pom.xml new file mode 100644 index 0000000000..1976274023 --- /dev/null +++ b/migration/pom.xml @@ -0,0 +1,75 @@ + + + + 4.0.0 + + io.javaoperatorsdk + java-operator-sdk + 5.3.1-SNAPSHOT + + + migration + Operator SDK - Migration Recipes + OpenRewrite migration recipes for Java Operator SDK + + + 8.46.1 + + + + + org.openrewrite + rewrite-java + ${openrewrite.version} + + + org.openrewrite + rewrite-maven + ${openrewrite.version} + + + org.openrewrite + rewrite-test + ${openrewrite.version} + test + + + org.openrewrite + rewrite-java-17 + ${openrewrite.version} + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + diff --git a/migration/src/main/java/io/javaoperatorsdk/operator/migration/RemoveMethodDeclaration.java b/migration/src/main/java/io/javaoperatorsdk/operator/migration/RemoveMethodDeclaration.java new file mode 100644 index 0000000000..33841d5acc --- /dev/null +++ b/migration/src/main/java/io/javaoperatorsdk/operator/migration/RemoveMethodDeclaration.java @@ -0,0 +1,150 @@ +/* + * 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.migration; + +import java.util.Objects; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.NlsRewrite; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +public class RemoveMethodDeclaration extends Recipe { + + @Option( + displayName = "Interface name", + description = "Fully qualified or simple name of the interface.", + example = "com.example.YourInterface") + String interfaceName; + + @Option( + displayName = "Method name", + description = "Name of the method to remove.", + example = "removedMethod") + String methodName; + + @Override + public String getDisplayName() { + return "Remove obsolete method from implementing classes"; + } + + @Override + public @NlsRewrite.Description String getDescription() { + return "Remove obsolete method from implementing classes"; + } + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + + @Override + public J.ClassDeclaration visitClassDeclaration( + J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + + if (cd.getType() == null || !typeMatchesOrImplements(cd.getType())) { + return cd; + } + + // Mutate the type info in place to remove the method from the declared methods list, + // so all AST nodes sharing this type reference stay consistent. + var type = cd.getType(); + if (type instanceof JavaType.Class classType) { + var updatedMethods = + classType.getMethods().stream().filter(m -> !m.getName().equals(methodName)).toList(); + classType.unsafeSet( + classType.getTypeParameters(), + classType.getSupertype(), + classType.getOwningClass(), + classType.getAnnotations(), + classType.getInterfaces(), + classType.getMembers(), + updatedMethods); + } + + return cd; + } + + @Override + public J.MethodDeclaration visitMethodDeclaration( + J.MethodDeclaration method, ExecutionContext ctx) { + if (!method.getSimpleName().equals(methodName)) { + return super.visitMethodDeclaration(method, ctx); + } + + J.ClassDeclaration classDecl = getCursor().firstEnclosing(J.ClassDeclaration.class); + if (classDecl == null || classDecl.getType() == null) { + return super.visitMethodDeclaration(method, ctx); + } + + if (typeMatchesOrImplements(classDecl.getType())) { + //noinspection DataFlowIssue + return null; + } + + return super.visitMethodDeclaration(method, ctx); + } + + private boolean typeMatchesOrImplements(JavaType.FullyQualified type) { + for (var iface : type.getInterfaces()) { + if (iface.getFullyQualifiedName().equals(interfaceName) + || typeMatchesOrImplements(iface)) { + return true; + } + } + var supertype = type.getSupertype(); + if (supertype != null && !supertype.getFullyQualifiedName().equals("java.lang.Object")) { + return typeMatchesOrImplements(supertype); + } + return false; + } + }; + } + + public String getInterfaceName() { + return interfaceName; + } + + public void setInterfaceName(String interfaceName) { + this.interfaceName = interfaceName; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + RemoveMethodDeclaration that = (RemoveMethodDeclaration) o; + return Objects.equals(interfaceName, that.interfaceName) + && Objects.equals(methodName, that.methodName); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), interfaceName, methodName); + } +} diff --git a/migration/src/main/resources/META-INF/rewrite/v5-3-migration.yml b/migration/src/main/resources/META-INF/rewrite/v5-3-migration.yml new file mode 100644 index 0000000000..cab26f1916 --- /dev/null +++ b/migration/src/main/resources/META-INF/rewrite/v5-3-migration.yml @@ -0,0 +1,124 @@ +# +# 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. +# + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.javaoperatorsdk.operator.migration.V5_3Migration +displayName: Migrate to Java Operator SDK v5.3 +description: >- + Migrates Java Operator SDK from v5.2 to v5.3, including the JUnit module + rename and Metrics interface method renames. +recipeList: + - io.javaoperatorsdk.operator.migration.UpgradeJOSDKVersion + - io.javaoperatorsdk.operator.migration.RenameJUnitModule + - io.javaoperatorsdk.operator.migration.MetricsMethodRenames + - io.javaoperatorsdk.operator.migration.RemoveMonitorSizeOf + - io.javaoperatorsdk.operator.migration.RelocateResourceAction +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.javaoperatorsdk.operator.migration.UpgradeJOSDKVersion +displayName: Upgrade Java Operator SDK version to 5.3.0 +description: >- + Upgrades all io.javaoperatorsdk dependencies from 5.2.x to 5.3.0. +recipeList: + - org.openrewrite.maven.UpgradeDependencyVersion: + groupId: io.javaoperatorsdk + artifactId: "*" + newVersion: 5.3.0 + versionPattern: "5.2.*" +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.javaoperatorsdk.operator.migration.RenameJUnitModule +displayName: Rename JUnit module artifact +description: >- + Renames the operator-framework-junit-5 artifact to operator-framework-junit. +recipeList: + - org.openrewrite.maven.ChangeDependencyGroupIdAndArtifactId: + oldGroupId: io.javaoperatorsdk + oldArtifactId: operator-framework-junit-5 + newArtifactId: operator-framework-junit + newVersion: 5.3.0 +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.javaoperatorsdk.operator.migration.MetricsMethodRenames +displayName: Rename Metrics interface methods +description: >- + Renames methods on the Metrics interface to match the new v5.3 API. +recipeList: + - org.openrewrite.java.ChangeMethodName: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics reconcileCustomResource(..)" + newMethodName: reconciliationSubmitted + matchOverrides: true + - org.openrewrite.java.ChangeMethodName: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics reconciliationExecutionStarted(..)" + newMethodName: reconciliationStarted + matchOverrides: true + - org.openrewrite.java.ChangeMethodName: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics reconciliationExecutionFinished(..)" + newMethodName: reconciliationSucceeded + matchOverrides: true + - org.openrewrite.java.AddNullMethodArgument: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics failedReconciliation(..)" + argumentIndex: 1 + parameterType: io.javaoperatorsdk.operator.api.reconciler.RetryInfo + - org.openrewrite.java.ChangeMethodName: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics failedReconciliation(..)" + newMethodName: reconciliationFailed + matchOverrides: true + - org.openrewrite.java.AddMethodParameter: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics reconciliationFailed(..)" + parameterType: io.javaoperatorsdk.operator.api.reconciler.RetryInfo + parameterName: retryInfo + parameterIndex: 1 + - org.openrewrite.java.AddNullMethodArgument: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics finishedReconciliation(..)" + argumentIndex: 1 + parameterType: io.javaoperatorsdk.operator.api.reconciler.RetryInfo + - org.openrewrite.java.ChangeMethodName: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics finishedReconciliation(..)" + newMethodName: reconciliationFinished + matchOverrides: true + - org.openrewrite.java.AddMethodParameter: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics reconciliationFinished(..)" + parameterType: io.javaoperatorsdk.operator.api.reconciler.RetryInfo + parameterName: retryInfo + parameterIndex: 1 + - org.openrewrite.java.ChangeMethodName: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics cleanupDoneFor(..)" + newMethodName: cleanupDone + matchOverrides: true + - org.openrewrite.java.ChangeMethodName: + methodPattern: "io.javaoperatorsdk.operator.api.monitoring.Metrics receivedEvent(..)" + newMethodName: eventReceived + matchOverrides: true +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.javaoperatorsdk.operator.migration.RemoveMonitorSizeOf +displayName: Remove MonitorSizeOf +recipeList: + - io.javaoperatorsdk.operator.migration.RemoveMethodDeclaration: + interfaceName: io.javaoperatorsdk.operator.api.monitoring.Metrics + methodName: monitorSizeOf +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.javaoperatorsdk.operator.migration.RelocateResourceAction +displayName: Relocate ResourceAction class +description: >- + Moves ResourceAction import from the controller sub-package to the event source package. +recipeList: + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction + newFullyQualifiedTypeName: io.javaoperatorsdk.operator.processing.event.source.ResourceAction diff --git a/migration/src/test/java/io/javaoperatorsdk/operator/migration/V53MigrationTest.java b/migration/src/test/java/io/javaoperatorsdk/operator/migration/V53MigrationTest.java new file mode 100644 index 0000000000..62ab4a412b --- /dev/null +++ b/migration/src/test/java/io/javaoperatorsdk/operator/migration/V53MigrationTest.java @@ -0,0 +1,393 @@ +/* + * 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.migration; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.maven.Assertions.pomXml; + +class V53MigrationTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipeFromResources("io.javaoperatorsdk.operator.migration.V5_3Migration"); + } + + @Test + void renamesJUnitModuleInMaven() { + rewriteRun( + pomXml( + """ + + 4.0.0 + com.example + test + 1.0 + + + io.javaoperatorsdk + operator-framework-junit-5 + 5.2.0 + test + + + + """, + """ + + 4.0.0 + com.example + test + 1.0 + + + io.javaoperatorsdk + operator-framework-junit + 5.3.0 + test + + + + """)); + } + + @Test + void renamesMetricsMethods() { + rewriteRun( + // language=java + java( + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import java.util.Map; + + public interface Metrics { + default void receivedEvent(Object event, Map metadata) {} + default void reconcileCustomResource(Object resource, Object retryInfo, Map metadata) {} + default void reconciliationExecutionStarted(Object resource, Map metadata) {} + default void reconciliationExecutionFinished(Object resource, Map metadata) {} + default void failedReconciliation(Object resource, Exception exception, Map metadata) {} + default void finishedReconciliation(Object resource, Map metadata) {} + default void cleanupDoneFor(Object resourceID, Map metadata) {} + } + """, + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; + + import java.util.Map; + + public interface Metrics { + default void eventReceived(Object event, Map metadata) {} + default void reconciliationSubmitted(Object resource, Object retryInfo, Map metadata) {} + default void reconciliationStarted(Object resource, Map metadata) {} + default void reconciliationSucceeded(Object resource, Map metadata) {} + + default void reconciliationFailed(Object resource, RetryInfo retryInfo, Exception exception, Map metadata) {} + + default void reconciliationFinished(Object resource, RetryInfo retryInfo, Map metadata) {} + default void cleanupDone(Object resourceID, Map metadata) {} + } + """)); + } + + @Test + void renamesMetricsMethodCallsInImplementation() { + rewriteRun( + // Stub for the Metrics interface + // language=java + java( + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import java.util.Map; + + public interface Metrics { + default void receivedEvent(Object event, Map metadata) {} + default void reconcileCustomResource(Object resource, Object retryInfo, Map metadata) {} + } + """, + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import java.util.Map; + + public interface Metrics { + default void eventReceived(Object event, Map metadata) {} + default void reconciliationSubmitted(Object resource, Object retryInfo, Map metadata) {} + } + """), + // Implementation that overrides the old method names + // language=java + java( + """ + package com.example; + + import java.util.Map; + import io.javaoperatorsdk.operator.api.monitoring.Metrics; + + public class MyMetrics implements Metrics { + @Override + public void receivedEvent(Object event, Map metadata) { + System.out.println("event received"); + } + + @Override + public void reconcileCustomResource(Object resource, Object retryInfo, Map metadata) { + System.out.println("reconcile"); + } + } + """, + """ + package com.example; + + import java.util.Map; + import io.javaoperatorsdk.operator.api.monitoring.Metrics; + + public class MyMetrics implements Metrics { + @Override + public void eventReceived(Object event, Map metadata) { + System.out.println("event received"); + } + + @Override + public void reconciliationSubmitted(Object resource, Object retryInfo, Map metadata) { + System.out.println("reconcile"); + } + } + """)); + } + + @Test + void removesMonitorSizeOfFromImplementationWithGenerics() { + rewriteRun( + // Stub for the Metrics interface (unchanged, from JOSDK library) + // language=java + java( + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import java.util.Map; + + public interface Metrics { + default void eventReceived(Object event, Map metadata) {} + default > T monitorSizeOf(T map, String name) { return map; } + } + """), + // Implementation that overrides monitorSizeOf with generic signature + // language=java + java( + """ + package com.example; + + import java.util.Map; + import io.javaoperatorsdk.operator.api.monitoring.Metrics; + + public class MyMetrics implements Metrics { + @Override + public void eventReceived(Object event, Map metadata) { + System.out.println("event"); + } + + @Override + public > T monitorSizeOf(T map, String name) { + System.out.println("monitoring size"); + return map; + } + } + """, + """ + package com.example; + + import java.util.Map; + import io.javaoperatorsdk.operator.api.monitoring.Metrics; + + public class MyMetrics implements Metrics { + @Override + public void eventReceived(Object event, Map metadata) { + System.out.println("event"); + } + } + """), + // Implementation without @Override annotation + // language=java + java( + """ + package com.example; + + import java.util.Map; + import io.javaoperatorsdk.operator.api.monitoring.Metrics; + + public class AnotherMetrics implements Metrics { + public > T monitorSizeOf(T map, String name) { + return map; + } + } + """, + """ + package com.example; + + import java.util.Map; + import io.javaoperatorsdk.operator.api.monitoring.Metrics; + + public class AnotherMetrics implements Metrics { + } + """)); + } + + @Test + void addsRetryInfoParameterToReconciliationFinished() { + rewriteRun( + // language=java + java( + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import java.util.Map; + + public interface Metrics { + default void finishedReconciliation(Object resource, Map metadata) {} + } + """, + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; + + import java.util.Map; + + public interface Metrics { + default void reconciliationFinished(Object resource, RetryInfo retryInfo, Map metadata) {} + } + """)); + } + + @Test + void addsNullRetryInfoArgumentToInvocations() { + rewriteRun( + // Stub for the Metrics interface with old method names + // language=java + java( + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import java.util.Map; + + public interface Metrics { + default void failedReconciliation(Object resource, Exception exception, Map metadata) {} + default void finishedReconciliation(Object resource, Map metadata) {} + } + """, + """ + package io.javaoperatorsdk.operator.api.monitoring; + + import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; + + import java.util.Map; + + public interface Metrics { + default void reconciliationFailed(Object resource, RetryInfo retryInfo, Exception exception, Map metadata) {} + + default void reconciliationFinished(Object resource, RetryInfo retryInfo, Map metadata) {} + } + """), + // Stub for RetryInfo + // language=java + java( + """ + package io.javaoperatorsdk.operator.api.reconciler; + public interface RetryInfo {} + """), + // Class that calls the old methods + // language=java + java( + """ + package com.example; + + import java.util.Map; + import io.javaoperatorsdk.operator.api.monitoring.Metrics; + + public class MetricsCaller { + public void report(Metrics metrics, Object resource, Exception ex, Map meta) { + metrics.failedReconciliation(resource, ex, meta); + metrics.finishedReconciliation(resource, meta); + } + } + """, + """ + package com.example; + + import java.util.Map; + import io.javaoperatorsdk.operator.api.monitoring.Metrics; + + public class MetricsCaller { + public void report(Metrics metrics, Object resource, Exception ex, Map meta) { + metrics.reconciliationFailed(resource, null, ex, meta); + metrics.reconciliationFinished(resource, null, meta); + } + } + """)); + } + + @Test + void relocatesResourceActionImport() { + rewriteRun( + // Stub for the old ResourceAction location + // language=java + java( + """ + package io.javaoperatorsdk.operator.processing.event.source.controller; + + public class ResourceAction { + } + """, + """ + package io.javaoperatorsdk.operator.processing.event.source; + + public class ResourceAction { + } + """), + // Class that imports ResourceAction from the old package + // language=java + java( + """ + package com.example; + + import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; + + public class MyHandler { + public void handle(ResourceAction action) { + System.out.println(action); + } + } + """, + """ + package com.example; + + import io.javaoperatorsdk.operator.processing.event.source.ResourceAction; + + public class MyHandler { + public void handle(ResourceAction action) { + System.out.println(action); + } + } + """)); + } +} diff --git a/pom.xml b/pom.xml index 9ee9155dbc..d1562cb1a0 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ caffeine-bounded-cache-support bootstrapper-maven-plugin test-index-processor + migration