Skip to content

Native Image Friendly #231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ public class Runner {
The Controller implements the business logic and describes all the classes needed to handle the CRD.

```java
@Controller(customResourceClass = WebServer.class,
crdName = "webservers.sample.javaoperatorsdk")
@Controller(crdName = "webservers.sample.javaoperatorsdk")
public class WebServerController implements ResourceController<WebServer> {

@Override
Expand Down
23 changes: 20 additions & 3 deletions operator-framework/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
</plugins>
</build>


<dependencies>
<dependency>
<groupId>io.fabric8</groupId>
Expand Down Expand Up @@ -77,10 +78,26 @@
<version>4.0.3</version>
<scope>test</scope>
</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>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
<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
@@ -0,0 +1,56 @@
package io.javaoperatorsdk.operator;

import io.fabric8.kubernetes.client.CustomResource;
import io.javaoperatorsdk.operator.api.ResourceController;
import org.apache.commons.lang3.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;


class ControllerToCustomResourceMappingsProvider {
private static final Logger log = LoggerFactory.getLogger(ControllerUtils.class);

static Map<Class<? extends ResourceController>, Class<? extends CustomResource>> provide(final String resourcePath) {
Map<Class<? extends ResourceController>, Class<? extends CustomResource>> controllerToCustomResourceMappings = new HashMap();
try {
final Enumeration<URL> customResourcesMetadataList = ControllerUtils.class.getClassLoader().getResources(resourcePath);
for (Iterator<URL> it = customResourcesMetadataList.asIterator(); it.hasNext(); ) {
URL url = it.next();

List<String> classNamePairs = retrieveClassNamePairs(url);
classNamePairs.forEach(clazzPair -> {
try {
final String[] classNames = clazzPair.split(",");
if (classNames.length != 2) {
throw new IllegalStateException(String.format("%s is not valid CustomResource metadata defined in %s", clazzPair, url.toString()));
}

controllerToCustomResourceMappings.put(
(Class<? extends ResourceController>) ClassUtils.getClass(classNames[0]),
(Class<? extends CustomResource>) ClassUtils.getClass(classNames[1])
);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
});
}
log.debug("Loaded Controller to CustomResource mappings {}", controllerToCustomResourceMappings);
return controllerToCustomResourceMappings;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static List<String> retrieveClassNamePairs(URL url) throws IOException {
return new BufferedReader(
new InputStreamReader(url.openStream())
).lines().collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
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.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";
public static final String CONTROLLERS_RESOURCE_PATH = "javaoperatorsdk/controllers";
private static Map<Class<? extends ResourceController>, Class<? extends CustomResource>> controllerToCustomResourceMappings;

// this is just to support testing, this way we don't try to create class multiple times in memory with same name.
// note that other solution is to add a random string to doneable class name
private static Map<Class<? extends CustomResource>, Class<? extends CustomResourceDoneable<? extends CustomResource>>>
doneableClassCache = new HashMap<>();
static {
controllerToCustomResourceMappings =
ControllerToCustomResourceMappingsProvider
.provide(CONTROLLERS_RESOURCE_PATH);
}

static String getFinalizer(ResourceController controller) {
final String annotationFinalizerName = getAnnotation(controller).finalizerName();
if (!Controller.NULL.equals(annotationFinalizerName)) {
return annotationFinalizerName;
}
final String crdName = getAnnotation(controller).crdName() + FINALIZER_NAME_SUFFIX;
return crdName;
return getAnnotation(controller).crdName() + FINALIZER_NAME_SUFFIX;
}

static boolean getGenerationEventProcessing(ResourceController controller) {
static boolean getGenerationEventProcessing(ResourceController<?> controller) {
return getAnnotation(controller).generationAwareEventProcessing();
}

static <R extends CustomResource> Class<R> getCustomResourceClass(ResourceController<R> controller) {
return (Class<R>) getAnnotation(controller).customResourceClass();
final Class<? extends CustomResource> customResourceClass = controllerToCustomResourceMappings
.get(controller.getClass());
if (customResourceClass == null) {
throw new IllegalArgumentException(
String.format(
"No custom resource has been found for controller %s",
controller.getClass().getCanonicalName()
)
);
}
return (Class<R>) customResourceClass;
}

static String getCrdName(ResourceController controller) {
Expand All @@ -48,38 +54,15 @@ 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;
}
}

private static Controller getAnnotation(ResourceController controller) {
private static Controller getAnnotation(ResourceController<?> controller) {
return controller.getClass().getAnnotation(Controller.class);
}

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

import io.fabric8.kubernetes.client.CustomResource;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -14,8 +13,6 @@

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,150 @@
package io.javaoperatorsdk.operator.processing;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.*;
import io.fabric8.kubernetes.api.builder.Function;
import io.fabric8.kubernetes.client.CustomResourceDoneable;
import io.javaoperatorsdk.operator.api.ResourceController;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static io.javaoperatorsdk.operator.ControllerUtils.CONTROLLERS_RESOURCE_PATH;

@SupportedAnnotationTypes(
"io.javaoperatorsdk.operator.api.Controller")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class ControllerAnnotationProcessor extends AbstractProcessor {
private FileObject resource;
PrintWriter printWriter = null;
private Set<String> generatedDoneableClassFiles = new HashSet<>();

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
try {
resource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", CONTROLLERS_RESOURCE_PATH);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
printWriter = new PrintWriter(resource.openOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements
= roundEnv.getElementsAnnotatedWith(annotation);
annotatedElements.stream().filter(element -> element.getKind().equals(ElementKind.CLASS))
.map(e -> (TypeElement) e)
.forEach(e -> this.generateDoneableClass(e, printWriter));
}
} finally {
printWriter.close();
}
return true;
}

private void generateDoneableClass(TypeElement controllerClassSymbol, PrintWriter printWriter) {
try {
final TypeMirror resourceType = findResourceType(controllerClassSymbol);

TypeElement customerResourceTypeElement = processingEnv
.getElementUtils()
.getTypeElement(resourceType.toString());

final String doneableClassName = customerResourceTypeElement.getSimpleName() + "Doneable";
final String destinationClassFileName = customerResourceTypeElement.getQualifiedName() + "Doneable";
final TypeName customResourceType = TypeName.get(resourceType);

if (!generatedDoneableClassFiles.add(destinationClassFileName)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
String.format(
"%s already exist! adding the mapping to the %s",
destinationClassFileName,
CONTROLLERS_RESOURCE_PATH)
);
printWriter.println(controllerClassSymbol.getQualifiedName() + "," + customResourceType.toString());
return;
}
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(destinationClassFileName);

try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
printWriter.println(controllerClassSymbol.getQualifiedName() + "," + customResourceType.toString());
final MethodSpec constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(customResourceType, "resource")
.addParameter(Function.class, "function")
.addStatement("super(resource,function)")
.build();


final TypeSpec typeSpec = TypeSpec.classBuilder(doneableClassName)
.superclass(ParameterizedTypeName.get(ClassName.get(CustomResourceDoneable.class), customResourceType))
.addModifiers(Modifier.PUBLIC)
.addMethod(constructor)
.build();

final PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(customerResourceTypeElement);
JavaFile file = JavaFile.builder(packageElement.getQualifiedName().toString(), typeSpec)
.build();
file.writeTo(out);
}
} catch (Exception ioException) {
ioException.printStackTrace();
}
}

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

return controllerType.getTypeArguments().get(0);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

private List<DeclaredType> collectAllInterfaces(TypeElement element) {
try {
List<DeclaredType> interfaces = new ArrayList<>(element.getInterfaces()).stream().map(t -> (DeclaredType) t).collect(Collectors.toList());
TypeElement superclass = ((TypeElement) ((DeclaredType) element.getSuperclass()).asElement());
while (superclass.getSuperclass().getKind() != TypeKind.NONE) {
interfaces.addAll(superclass.getInterfaces().stream().map(t -> (DeclaredType) t).collect(Collectors.toList()));
superclass = ((TypeElement) ((DeclaredType) superclass.getSuperclass()).asElement());
}
return interfaces;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
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
Loading