Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1e2efe0
Add an annotation processor to generate Doneable classes
Nov 19, 2020
0ffe8ef
Remove customResourceClass
Nov 19, 2020
6f6260e
Use reflection to get getCustomResourceDoneableClass
Nov 19, 2020
decfd9c
Add auto-service and quarkus runtime dependency
Nov 19, 2020
ede8325
remove customResourceClass from controller classes
Nov 19, 2020
8aa10d1
fix tests
Nov 19, 2020
9380215
Merge remote-tracking branch 'origin/master' into annotation-processo…
Nov 19, 2020
62fba5c
resolve conflict
Nov 19, 2020
41a10a2
extract generateDoneableClass
Nov 19, 2020
14167cf
Add tools.jar to boot classpath
Nov 19, 2020
4103622
Add tools.jar to boot classpath only for java8
Nov 19, 2020
795c656
Add tools.jar to boot classpath only for java8
Nov 19, 2020
2697810
Add javapoet
Nov 20, 2020
7075ddf
Generate the class with javapoet
Nov 20, 2020
ef3dd1a
Add compile-testing
Nov 20, 2020
d969187
Test annotation processor works correctly with more one interface
Nov 21, 2020
bf2cb03
Test annotation processor works correctly when an intermediary class …
Nov 21, 2020
7eaad18
don't case to ParameterizedTypeImpl
Nov 23, 2020
e02804f
remove doneableClassCache
Nov 23, 2020
be8468d
Remove the dependency to javassist
Nov 23, 2020
b207ba9
Get rid of com.sun... pacages
Nov 23, 2020
49272c7
Get rid of com.sun... packages
Nov 23, 2020
3121fce
fix destination path
Nov 23, 2020
bb89c00
Merge branch 'master' into annotation-processor-test
Nov 23, 2020
0f110e1
generate javaoperatorsdk-custom-resources file
Nov 25, 2020
9482857
load controllerToCustomResourceMappings map
Nov 25, 2020
e07d463
Merge remote-tracking branch 'origin/annotation-processor-test' into …
Nov 25, 2020
5446214
move the resource class metadata to sub directory called "javaoperato…
Nov 26, 2020
84a2b9c
remove unnecessary printlns
Nov 26, 2020
58334a6
Use url.open to read the resource streams
Nov 26, 2020
93f150a
remove java8 profile
Nov 26, 2020
99a541d
remove dependency to quarkus
Nov 26, 2020
e5909fa
extract custom resource mapping logic
Nov 26, 2020
5a0af91
Update the example in the Readme
Nov 26, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 76 additions & 10 deletions operator-framework/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,57 @@
<packaging>jar</packaging>

<properties>
<java.version>8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
</plugin>
</plugins>
</build>
<profiles>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't that be removed now that we only target Java 11+?

Copy link
Contributor

@kirek007 kirek007 Nov 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@psycho-ir, this should be removed, because we support only 11+ (it's already in master)

<profile>
<id>java8</id>
<activation>
<jdk>1.8</jdk>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still valid? I guess this needs to be java 11

</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArguments>
<bootclasspath>
${java.home}/lib/rt.jar${path.separator}${java.home}/lib/jce.jar${path.separator}${java.home}/../lib/tools.jar
</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
</plugin>
</plugins>
</build>
</profile>
</profiles>





<dependencies>
<dependency>
Expand Down Expand Up @@ -82,5 +119,34 @@
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>

<dependency>
<groupId>com.google.testing.compile</groupId>
<artifactId>compile-testing</artifactId>
<version>0.19</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc2</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove quarkus dependency, we'll add it in dedicated module

<artifactId>quarkus-core</artifactId>
<version>1.9.2.Final</version>
</dependency>

<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.13.0</version>
<scope>compile</scope>
</dependency>


</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package io.javaoperatorsdk.operator;

import io.javaoperatorsdk.operator.api.Controller;
import io.javaoperatorsdk.operator.api.ResourceController;
import io.fabric8.kubernetes.api.builder.Function;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.client.CustomResourceDoneable;
import javassist.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.javaoperatorsdk.operator.api.Controller;
import io.javaoperatorsdk.operator.api.ResourceController;

import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;


public class ControllerUtils {

private final static double JAVA_VERSION = Double.parseDouble(System.getProperty("java.specification.version"));
private static final String FINALIZER_NAME_SUFFIX = "/finalizer";

// this is just to support testing, this way we don't try to create class multiple times in memory with same name.
Expand All @@ -37,7 +34,14 @@ static boolean getGenerationEventProcessing(ResourceController controller) {
}

static <R extends CustomResource> Class<R> getCustomResourceClass(ResourceController<R> controller) {
return (Class<R>) getAnnotation(controller).customResourceClass();
final Class<R> type = Arrays
.stream(controller.getClass().getGenericInterfaces())
.filter(i -> i instanceof ParameterizedType)
.map(i -> (ParameterizedType) i)
.findFirst()
.map(i -> (Class<R>) i.getActualTypeArguments()[0])
.get();
return type;
}

static String getCrdName(ResourceController controller) {
Expand All @@ -48,34 +52,11 @@ static String getCrdName(ResourceController controller) {
public static <T extends CustomResource> Class<? extends CustomResourceDoneable<T>>
getCustomResourceDoneableClass(ResourceController<T> controller) {
try {
Class<? extends CustomResource> customResourceClass = getAnnotation(controller).customResourceClass();
String className = customResourceClass.getPackage().getName() + "." + customResourceClass.getSimpleName() + "CustomResourceDoneable";

if (doneableClassCache.containsKey(customResourceClass)) {
return (Class<? extends CustomResourceDoneable<T>>) doneableClassCache.get(customResourceClass);
}

ClassPool pool = ClassPool.getDefault();
pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));

CtClass superClass = pool.get(CustomResourceDoneable.class.getName());
CtClass function = pool.get(Function.class.getName());
CtClass customResource = pool.get(customResourceClass.getName());
CtClass[] argTypes = {customResource, function};
CtClass customDoneable = pool.makeClass(className, superClass);
CtConstructor ctConstructor = CtNewConstructor.make(argTypes, null, "super($1, $2);", customDoneable);
customDoneable.addConstructor(ctConstructor);

Class<? extends CustomResourceDoneable<T>> doneableClass;
if (JAVA_VERSION >= 9) {
doneableClass = (Class<? extends CustomResourceDoneable<T>>) customDoneable.toClass(customResourceClass);
} else {
doneableClass = (Class<? extends CustomResourceDoneable<T>>) customDoneable.toClass();
}
doneableClassCache.put(customResourceClass, doneableClass);
return doneableClass;
} catch (CannotCompileException | NotFoundException e) {
throw new IllegalStateException(e);
final Class<T> customResourceClass = getCustomResourceClass(controller);
return (Class<? extends CustomResourceDoneable<T>>) Class.forName(customResourceClass.getCanonicalName() + "Doneable");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this work if the CR class is an inner class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does work, haven't tested that with native-images yet though.
@kirek007 do you think we can have an integration test in example repository where has the CustomResource as inner class?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll add this test

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After while, I believe that we should test it here, not in example. We would like to introduce native build as a main feature of the SDK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add native build as a test step in next PR.
WDYT: @adam-sandor @csviri ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After while, I believe that we should test it here, not in example. We would like to introduce native build as a main feature of the SDK.

True, would be great to have it as one of the examples in this repository, will be easier to find for the users and also can be used as regression tests for future changes in the framework.

} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
package io.javaoperatorsdk.operator.api;

import io.fabric8.kubernetes.client.CustomResource;
import io.quarkus.runtime.annotations.RegisterForReflection;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@RegisterForReflection
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only reason we have quarkus on classpath? Would be cleaner if we could do it without it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if somebody wan't to use quarkus, it can be added to the project. But if not its an unnecessary dependency on classpath.
However if there is no other way to do this, then we can let it here, since quarkus is a priority.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, good point.
We can create project that will act like "springboot-starter" for quarkus. It will take of usign supported quarkus version and configuration of reflections.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'm planning on doing that with #228.

Copy link
Contributor

@kirek007 kirek007 Nov 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So my suggestion is to:

  1. Remove quarkus from this PR
  2. Because WIP: feat: make it possible to externally configure the operator #228 is getting quite big let's open new PR dedicated only for implementing quarkus starter
    If @metacosm don't mind I can take care of this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, I'm not sure that native compilation will work properly without using RegisterForReflection… I also think we should merge #229 as soon as possible because that will simplify a lot of things by removing the need to deal with Doneable classes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kirek007 another aspect is also that #228 tries to extract the configuration parts from the Spring Boot starter so that it could be reused in a Quarkus extension instead of duplicating the code so I really think that it should get done sooner rather than later. That said, I'm not quite satisfied with how it currently is, in terms of configuration I mean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove the Quarkus dependency from here, the other way to achieve the same is generating a ReflectionConfigurationFile in compile time (just like what Quarkus is doing with the annotations). Can be done in the new module that will be added.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be great @psycho-ir

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is related to quarkus dependency and should be removed

@Target({ElementType.TYPE})
public @interface Controller {
String NULL = "";

String crdName();

Class<? extends CustomResource> customResourceClass();

/**
* Optional finalizer name, if it is not,
* the crdName will be used as the name of the finalizer too.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.javaoperatorsdk.operator.processing;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.*;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import io.fabric8.kubernetes.api.builder.Function;
import io.fabric8.kubernetes.client.CustomResourceDoneable;
import io.javaoperatorsdk.operator.api.ResourceController;
import io.quarkus.runtime.annotations.RegisterForReflection;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@SupportedAnnotationTypes(
"io.javaoperatorsdk.operator.api.Controller")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class ControllerAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements
= roundEnv.getElementsAnnotatedWith(annotation);
annotatedElements.stream().filter(element -> element instanceof Symbol.ClassSymbol)
.map(e -> (Symbol.ClassSymbol) e)
.forEach(this::generateDoneableClass);
}
return false;
}

private void generateDoneableClass(Symbol.ClassSymbol controllerClassSymbol) {
try {
final TypeMirror resourceType = findResourceType(controllerClassSymbol);
Symbol.ClassSymbol customerResourceSymbol = (Symbol.ClassSymbol) processingEnv
.getElementUtils()
.getTypeElement(resourceType.toString());

JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(customerResourceSymbol.className() + "Doneable");

try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
final MethodSpec constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.get(resourceType), "resource")
.addParameter(Function.class, "function")
.addStatement("super(resource,function)")
.build();

final TypeSpec typeSpec = TypeSpec.classBuilder(customerResourceSymbol.name + "Doneable")
.addAnnotation(RegisterForReflection.class)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation won't compile after removing quarkus-core

.superclass(ParameterizedTypeName.get(ClassName.get(CustomResourceDoneable.class), TypeName.get(resourceType)))
.addModifiers(Modifier.PUBLIC)
.addMethod(constructor)
.build();

JavaFile file = JavaFile.builder(customerResourceSymbol.packge().fullname.toString(), typeSpec)
.build();
file.writeTo(out);
}
} catch (IOException ioException) {
ioException.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
}
}

private TypeMirror findResourceType(Symbol.ClassSymbol controllerClassSymbol) throws Exception {
final Type controllerType = collectAllInterfaces(controllerClassSymbol)
.stream()
.filter(i -> i.toString()
.startsWith(ResourceController.class.getCanonicalName())
)
.findFirst()
.orElseThrow(() -> new Exception("ResourceController is not implemented by " + controllerClassSymbol.toString()));

final TypeMirror resourceType = controllerType.getTypeArguments().get(0);
return resourceType;
}

private List<Type> collectAllInterfaces(Symbol.ClassSymbol classSymbol) {
List<Type> interfaces = new ArrayList<>(classSymbol.getInterfaces());
Symbol.ClassSymbol superclass = (Symbol.ClassSymbol) processingEnv.getTypeUtils().asElement(classSymbol.getSuperclass());

while (superclass != null) {
interfaces.addAll(superclass.getInterfaces());
superclass = (Symbol.ClassSymbol) processingEnv.getTypeUtils().asElement(superclass.getSuperclass());
}

return interfaces;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void returnsValuesFromControllerAnnotationFinalizer() {
assertTrue(CustomResourceDoneable.class.isAssignableFrom(ControllerUtils.getCustomResourceDoneableClass(new TestCustomResourceController(null))));
}

@Controller(crdName = "test.crd", customResourceClass = TestCustomResource.class, finalizerName = CUSTOM_FINALIZER_NAME)
@Controller(crdName = "test.crd", finalizerName = CUSTOM_FINALIZER_NAME)
static class TestCustomFinalizerController implements ResourceController<TestCustomResource> {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.javaoperatorsdk.operator.processing;

import com.google.testing.compile.*;
import com.google.testing.compile.Compiler;
import org.junit.jupiter.api.Test;

import javax.tools.JavaFileObject;

class ControllerAnnotationProcessorTest {
@Test
public void generateCorrectDoneableClassIfInterfaceIsSecond() {
Compilation compilation = Compiler.javac()
.withProcessors(new ControllerAnnotationProcessor())
.compile(JavaFileObjects.forResource("ControllerImplemented2Interfaces.java"));
CompilationSubject.assertThat(compilation).succeeded();

final JavaFileObject expectedResource = JavaFileObjects.forResource("ControllerImplemented2InterfacesExpected.java");
JavaFileObjectSubject.assertThat(compilation.generatedSourceFiles().get(0)).hasSourceEquivalentTo(expectedResource);
}

@Test
public void generateCorrectDoneableClassIfThereIsAbstractBaseController() {

Compilation compilation = Compiler.javac()
.withProcessors(new ControllerAnnotationProcessor())
.compile(
JavaFileObjects.forResource("AbstractController.java"),
JavaFileObjects.forResource("ControllerImplementedIntermediateAbstractClass.java")
);
CompilationSubject.assertThat(compilation).succeeded();

final JavaFileObject expectedResource = JavaFileObjects.forResource("ControllerImplementedIntermediateAbstractClassExpected.java");
JavaFileObjectSubject.assertThat(compilation.generatedSourceFiles().get(0)).hasSourceEquivalentTo(expectedResource);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@

@Controller(
generationAwareEventProcessing = false,
crdName = TestCustomResourceController.CRD_NAME,
customResourceClass = TestCustomResource.class)
crdName = TestCustomResourceController.CRD_NAME)
public class TestCustomResourceController implements ResourceController<TestCustomResource>, TestExecutionInfoProvider {

private static final Logger log = LoggerFactory.getLogger(TestCustomResourceController.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

@Controller(
crdName = SubResourceTestCustomResourceController.CRD_NAME,
customResourceClass = SubResourceTestCustomResource.class,
generationAwareEventProcessing = false)
public class SubResourceTestCustomResourceController implements ResourceController<SubResourceTestCustomResource>,
TestExecutionInfoProvider {
Expand Down
Loading