From 5953136d9db96c1c9ccf67d13be8a27a4700ca8b Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sat, 20 Sep 2025 21:01:12 -0400 Subject: [PATCH 1/5] feat(docs): add @Sample annotation for documentation Marks integration test classes so that markdown pages can be auto generated. --- operator-annotations/pom.xml | 20 ++++++++++++++ .../main/java/io/javaoperatorsdk/Sample.java | 26 +++++++++++++++++++ pom.xml | 1 + 3 files changed, 47 insertions(+) create mode 100644 operator-annotations/pom.xml create mode 100644 operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java diff --git a/operator-annotations/pom.xml b/operator-annotations/pom.xml new file mode 100644 index 0000000000..41e94777ce --- /dev/null +++ b/operator-annotations/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + io.javaoperatorsdk + java-operator-sdk + 5.1.3-SNAPSHOT + + + operator-annotations + + + 22 + 22 + UTF-8 + + + \ No newline at end of file diff --git a/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java b/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java new file mode 100644 index 0000000000..49c6d9bc20 --- /dev/null +++ b/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk; + +import java.lang.annotation.*; + +/** + * This annotation marks an integration test class as a sample for the documentation. + * Intended for use on test classes only. + * + *

Example: + *

{@code
+ * @Sample(
+ *  tldr="Usage of PrimaryToSecondaryMapper",
+ *  description="Showcases the usage of PrimaryToSecondaryMapper, in what situation it needs to be used and how to optimize typical uses with Informer indexes."
+ * )
+ * class PrimaryToSecondaryIT {
+ *   // details omitted
+ * }
+ * }
+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +@Documented +public @interface Sample { + String tldr(); + String description(); +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2afcd8448a..69524810b7 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ sample-operators caffeine-bounded-cache-support bootstrapper-maven-plugin + operator-annotations From 8b6f7db2913136b10ee20e9a56bce64409355cfc Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 29 Sep 2025 02:20:45 -0400 Subject: [PATCH 2/5] refactor: remove unneeded properties in pom.xml refactor: remove unneeded properties in pom.xml --- operator-annotations/pom.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/operator-annotations/pom.xml b/operator-annotations/pom.xml index 41e94777ce..2dd41a32c9 100644 --- a/operator-annotations/pom.xml +++ b/operator-annotations/pom.xml @@ -12,8 +12,6 @@ operator-annotations - 22 - 22 UTF-8 From 7e9be4aa253601b06426fb58383ca062127f57f2 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 6 Oct 2025 03:13:53 -0400 Subject: [PATCH 3/5] feat: Implement SampleProcessor to generate md files Implemented a sample processor to scan all samples and write the tldr/description to a md file Moved Sample.java to the annotation package --- .../{ => annotation}/Sample.java | 2 +- .../processor/SampleProcessor.java | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) rename operator-annotations/src/main/java/io/javaoperatorsdk/{ => annotation}/Sample.java (94%) create mode 100644 operator-annotations/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java diff --git a/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java b/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java similarity index 94% rename from operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java rename to operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java index 49c6d9bc20..62a39b0e3b 100644 --- a/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java +++ b/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk; +package io.javaoperatorsdk.annotation; import java.lang.annotation.*; diff --git a/operator-annotations/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java b/operator-annotations/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java new file mode 100644 index 0000000000..6c7934189d --- /dev/null +++ b/operator-annotations/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java @@ -0,0 +1,96 @@ +package io.javaoperatorsdk.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.element.*; +import javax.lang.model.util.Types; +import javax.tools.FileObject; +import javax.tools.StandardLocation; +import java.io.IOException; +import java.io.Writer; +import java.util.*; + +/** + * Annotation processor that generates a markdown file listing all classes annotated with @Sample. + */ +@SupportedAnnotationTypes("io.javaoperatorsdk.annotation.Sample") +public class SampleProcessor extends AbstractProcessor { + + private record SampleInfo(String tldr, String description) {} + private final List samples = new ArrayList<>(); + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + + Types types = processingEnv.getTypeUtils(); + for (TypeElement annotation: annotations) { + // element has details about the class being annotated, but not the values + // ex: String tldr = ..., it knows it has a field called tldr but not what's assigned + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + // a mirror gives access to the values assigned to the fields of the annotation + // element.getAnnotation does not work since the retention is SOURCE + AnnotationMirror annotationMirror = element.getAnnotationMirrors().stream() + .filter(am -> types.isSameType(am.getAnnotationType(), annotation.asType())) + .findFirst() + .orElse(null); + + if (annotationMirror != null) { + String tldr = getString(annotationMirror.getElementValues(), "tldr"); + String description = getString(annotationMirror.getElementValues(), "description"); + + samples.add(new SampleInfo(tldr, description) ); + } + } + } + + if (roundEnv.processingOver()) { + // sort to keep the order stable + samples.sort(Comparator.comparing(SampleInfo::tldr, String.CASE_INSENSITIVE_ORDER)); + writeSampleMDFile(samples); + } + return false; + } + + /** + * + */ + private void writeSampleMDFile(List samples) { + try { + FileObject fileObject = processingEnv.getFiler() + .createResource(StandardLocation.CLASS_OUTPUT, "", "samples.md"); + + try(Writer writer = fileObject.openWriter();) { + writer.write("# Integration Test Samples \n"); + + for (SampleInfo sample : samples) { + writer.write("## " + sample.tldr() + "\n"); + writer.write(sample.description() + "\n\n"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts a string value from the annotation values map. + * @param vals the map of annotation values + * @param name the name of the field to extract + * @return the string value, or empty string if not found + */ + private String getString( + Map vals, String name) { + for (Map.Entry ev : vals.entrySet()) { + if (ev.getKey().getSimpleName().contentEquals(name)) { + Object value = ev.getValue().getValue(); + return value == null ? "" : value.toString(); + } + } + // should not happen since tldr and description are mandatory + return ""; + } +} From db5b9b254972aad51a1eb47853b340ebf55157bd Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 6 Oct 2025 05:15:13 -0400 Subject: [PATCH 4/5] fix: add meta-inf to run processor This commit also adds an example sample annotation to the CRDMappingInTestExtensionIT class to test the sample annotation processor. --- .../javax.annotation.processing.Processor | 1 + operator-framework/pom.xml | 26 +++++++++++++++++++ .../operator/CRDMappingInTestExtensionIT.java | 7 +++++ .../changenamespace/ChangeNamespaceIT.java | 7 +++++ 4 files changed, 41 insertions(+) create mode 100644 operator-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/operator-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/operator-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..a9fb2eb851 --- /dev/null +++ b/operator-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.javaoperatorsdk.processor.SampleProcessor diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 9324f16835..8fdaeeab26 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -84,6 +84,12 @@ kube-api-test-client-inject test + + io.javaoperatorsdk + operator-annotations + 5.1.3-SNAPSHOT + test + @@ -106,6 +112,26 @@ + + + default-testCompile + + testCompile + + test-compile + + + + io.javaoperatorsdk + operator-annotations + ${project.version} + + + + -proc:full + + + diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java index 9153ae4ff5..63c786e2c0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Kind; import io.fabric8.kubernetes.model.annotation.Version; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -21,6 +22,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Shows how to use the test extension to load CRDs from files.", + description = + "Shows how to use the test extension to load CRDs from files. This is useful when you want" + + " to test your operator with real CRDs, for example when you want to test validation" + + " or defaulting.") public class CRDMappingInTestExtensionIT { private final KubernetesClient client = new KubernetesClientBuilder().build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java index 67f65c64ca..3d343de795 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java @@ -14,6 +14,7 @@ import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.RegisteredController; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -21,6 +22,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Changing the namespaces being watched at runtime", + description = + "Demonstrates how to change the namespaces being watched by a controller at runtime," + + " including adding and removing namespaces as well as switching to watch all" + + " namespaces.") class ChangeNamespaceIT { public static final String TEST_RESOURCE_NAME_1 = "test1"; From 092a2de04a4dd5bad0c86c37770d9a26253cf7de Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sun, 26 Oct 2025 03:09:08 -0400 Subject: [PATCH 5/5] fix: add meta-inf to run processor This commit also adds an example sample annotation to the CRDMappingInTestExtensionIT class to test the sample annotation processor. --- operator-annotations/pom.xml | 11 ++++++++ .../javax.annotation.processing.Processor | 1 + operator-framework/pom.xml | 26 +++++++++++++++++++ .../operator/CRDMappingInTestExtensionIT.java | 7 +++++ .../changenamespace/ChangeNamespaceIT.java | 7 +++++ 5 files changed, 52 insertions(+) create mode 100644 operator-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/operator-annotations/pom.xml b/operator-annotations/pom.xml index 2dd41a32c9..d12538a00f 100644 --- a/operator-annotations/pom.xml +++ b/operator-annotations/pom.xml @@ -15,4 +15,15 @@ UTF-8 + + + + maven-compiler-plugin + 3.14.0 + + none + + + + \ No newline at end of file diff --git a/operator-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/operator-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..a9fb2eb851 --- /dev/null +++ b/operator-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.javaoperatorsdk.processor.SampleProcessor diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 9324f16835..8fdaeeab26 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -84,6 +84,12 @@ kube-api-test-client-inject test + + io.javaoperatorsdk + operator-annotations + 5.1.3-SNAPSHOT + test + @@ -106,6 +112,26 @@ + + + default-testCompile + + testCompile + + test-compile + + + + io.javaoperatorsdk + operator-annotations + ${project.version} + + + + -proc:full + + + diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java index 9153ae4ff5..63c786e2c0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Kind; import io.fabric8.kubernetes.model.annotation.Version; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -21,6 +22,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Shows how to use the test extension to load CRDs from files.", + description = + "Shows how to use the test extension to load CRDs from files. This is useful when you want" + + " to test your operator with real CRDs, for example when you want to test validation" + + " or defaulting.") public class CRDMappingInTestExtensionIT { private final KubernetesClient client = new KubernetesClientBuilder().build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java index 67f65c64ca..3d343de795 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java @@ -14,6 +14,7 @@ import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.RegisteredController; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -21,6 +22,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Changing the namespaces being watched at runtime", + description = + "Demonstrates how to change the namespaces being watched by a controller at runtime," + + " including adding and removing namespaces as well as switching to watch all" + + " namespaces.") class ChangeNamespaceIT { public static final String TEST_RESOURCE_NAME_1 = "test1";