diff --git a/operator-annotations/pom.xml b/operator-annotations/pom.xml
new file mode 100644
index 0000000000..d12538a00f
--- /dev/null
+++ b/operator-annotations/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+
+ io.javaoperatorsdk
+ java-operator-sdk
+ 5.1.3-SNAPSHOT
+
+
+ operator-annotations
+
+
+ UTF-8
+
+
+
+
+
+ maven-compiler-plugin
+ 3.14.0
+
+ none
+
+
+
+
+
\ No newline at end of file
diff --git a/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java b/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java
new file mode 100644
index 0000000000..62a39b0e3b
--- /dev/null
+++ b/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java
@@ -0,0 +1,26 @@
+package io.javaoperatorsdk.annotation;
+
+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/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 extends TypeElement> 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 extends ExecutableElement, ? extends AnnotationValue> vals, String name) {
+ for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> 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 "";
+ }
+}
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";
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