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 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 ""; + } +} 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