Skip to content
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

feat: allow CRD generation of all detected CRs #399

Merged
merged 7 commits into from
Sep 13, 2022
3 changes: 0 additions & 3 deletions .github/scripts/testJokeSample.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
NAMESPACE="${1}"

# Apply "3rd-party" CRD: joke is not an owned resource by our operator, so it's not generated
kubectl apply -f samples/joke/src/main/k8s/jokes.samples.javaoperatorsdk.io-v1.yml

# Test operator by creating a Joke Request resource
kubectl apply -f samples/joke/src/main/k8s/jokerequest.yml

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkiverse.operatorsdk.common;

import java.util.Map;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
Expand All @@ -13,7 +15,7 @@ public AnnotationConfigurableAugmentedClassInfo(ClassInfo classInfo) {
}

@Override
protected boolean augmentIfKept(IndexView index, Logger log) {
protected boolean augmentIfKept(IndexView index, Logger log, Map<String, Object> context) {
// record associated configuration class
associatedConfigurationClass = typeAt(0).name();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.quarkiverse.operatorsdk.common;

import static io.quarkiverse.operatorsdk.common.Constants.ANNOTATION_CONFIGURABLE;
import static io.quarkiverse.operatorsdk.common.Constants.CUSTOM_RESOURCE;
import static io.quarkiverse.operatorsdk.common.Constants.DEPENDENT_RESOURCE;
import static io.quarkiverse.operatorsdk.common.Constants.RECONCILER;

import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;

import org.jboss.jandex.DotName;
Expand All @@ -29,24 +32,39 @@ public static boolean isStatusNotVoid(String statusClassName) {
* @return a stream of {@link ReconcilerAugmentedClassInfo} providing information about processable reconcilers
*/
public static Stream<ReconcilerAugmentedClassInfo> getKnownReconcilers(IndexView index, Logger log) {
return getProcessableExtensionsOf(RECONCILER, index, log)
return getProcessableImplementationsOf(RECONCILER, index, log, Collections.emptyMap())
.map(ReconcilerAugmentedClassInfo.class::cast);
}

public static Stream<? extends SelectiveAugmentedClassInfo> getProcessableExtensionsOf(DotName extendedOrImplementedClass,
IndexView index, Logger log) {
return index.getAllKnownImplementors(extendedOrImplementedClass).stream()
public static Stream<? extends SelectiveAugmentedClassInfo> getProcessableImplementationsOf(DotName interfaceType,
IndexView index, Logger log, Map<String, Object> context) {
return getProcessableImplementationsOrExtensionsOf(interfaceType, index, log, context, true);
}

public static Stream<? extends SelectiveAugmentedClassInfo> getProcessableSubClassesOf(DotName classType,
IndexView index, Logger log, Map<String, Object> context) {
return getProcessableImplementationsOrExtensionsOf(classType, index, log, context, false);
}

private static Stream<? extends SelectiveAugmentedClassInfo> getProcessableImplementationsOrExtensionsOf(
DotName implementedOrExtendedClass,
IndexView index, Logger log, Map<String, Object> context, boolean isInterface) {
final var extensions = isInterface ? index.getAllKnownImplementors(implementedOrExtendedClass)
: index.getAllKnownSubclasses(implementedOrExtendedClass);
return extensions.stream()
.map(classInfo -> {
if (RECONCILER.equals(extendedOrImplementedClass)) {
if (RECONCILER.equals(implementedOrExtendedClass)) {
return new ReconcilerAugmentedClassInfo(classInfo);
} else if (DEPENDENT_RESOURCE.equals(extendedOrImplementedClass)) {
} else if (DEPENDENT_RESOURCE.equals(implementedOrExtendedClass)) {
return new DependentResourceAugmentedClassInfo(classInfo);
} else if (ANNOTATION_CONFIGURABLE.equals(extendedOrImplementedClass)) {
} else if (ANNOTATION_CONFIGURABLE.equals(implementedOrExtendedClass)) {
return new AnnotationConfigurableAugmentedClassInfo(classInfo);
} else if (CUSTOM_RESOURCE.equals(implementedOrExtendedClass)) {
return new ResourceTargetingAugmentedClassInfo(classInfo, null);
} else {
throw new IllegalArgumentException("Don't know how to process " + extendedOrImplementedClass);
throw new IllegalArgumentException("Don't know how to process " + implementedOrExtendedClass);
}
})
.filter(fci -> fci.keepAugmented(index, log));
.filter(fci -> fci.keepAugmented(index, log, context));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static io.quarkiverse.operatorsdk.common.Constants.DEPENDENT_RESOURCE;

import java.util.Map;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;
Expand All @@ -13,7 +15,7 @@ public DependentResourceAugmentedClassInfo(ClassInfo classInfo) {
}

@Override
protected boolean augmentIfKept(IndexView index, Logger log) {
protected boolean augmentIfKept(IndexView index, Logger log, Map<String, Object> context) {
// only need to check the secondary resource type since the primary should have already been processed with the associated reconciler
final var secondaryTypeDN = typeAt(0).name();
registerForReflection(secondaryTypeDN.toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.quarkiverse.operatorsdk.common;

import static io.quarkiverse.operatorsdk.common.ConfigurationUtils.annotationValueOrDefault;

import java.util.Locale;

import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;

import io.fabric8.kubernetes.api.Pluralize;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Kind;
import io.fabric8.kubernetes.model.annotation.Plural;
import io.fabric8.kubernetes.model.annotation.Singular;

public class HasMetadataUtils {

private static final DotName GROUP = DotName.createSimple(Group.class.getName());
private static final DotName PLURAL = DotName.createSimple(Plural.class.getName());
private static final DotName SINGULAR = DotName.createSimple(Singular.class.getName());
private static final DotName KIND = DotName.createSimple(Kind.class.getName());

public static String getFullResourceName(ClassInfo resourceCI) {
return HasMetadata.getFullResourceName(getPlural(resourceCI), getGroup(resourceCI));
}

public static String getPlural(ClassInfo resourceCI) {
return annotationValueOrDefault(resourceCI.classAnnotation(PLURAL),
"value",
value -> value.asString().toLowerCase(Locale.ROOT),
() -> Pluralize.toPlural(getSingular(resourceCI)));
}

public static String getGroup(ClassInfo resourceCI) {
return annotationValueOrDefault(resourceCI.classAnnotation(GROUP), "value",
AnnotationValue::asString, () -> null);
}

public static String getSingular(ClassInfo resourceCI) {
return annotationValueOrDefault(resourceCI.classAnnotation(SINGULAR), "value",
AnnotationValue::asString, () -> getKind(resourceCI).toLowerCase(Locale.ROOT));
}

public static String getKind(ClassInfo resourceCI) {
return annotationValueOrDefault(resourceCI.classAnnotation(KIND),
"value", AnnotationValue::asString, resourceCI::simpleName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.quarkiverse.operatorsdk.common;

import io.fabric8.kubernetes.api.model.HasMetadata;

public interface LoadableResourceHolder<T extends HasMetadata> {
String getAssociatedResourceTypeName();

Class<T> loadAssociatedClass();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@
import static io.quarkiverse.operatorsdk.common.Constants.HAS_METADATA;
import static io.quarkiverse.operatorsdk.common.Constants.RECONCILER;

import java.util.Map;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;

import io.fabric8.kubernetes.api.model.HasMetadata;

/**
* Metadata about a processable reconciler implementation.
*/
public class ReconcilerAugmentedClassInfo extends SelectiveAugmentedClassInfo {
public class ReconcilerAugmentedClassInfo extends SelectiveAugmentedClassInfo implements LoadableResourceHolder<HasMetadata> {

private final String name;
private boolean isCR;
private boolean hasNonVoidStatus;
private SimpleLoadableResourceHolder<HasMetadata> holder;

public ReconcilerAugmentedClassInfo(ClassInfo classInfo) {
super(classInfo, RECONCILER, 1);
Expand All @@ -32,8 +37,8 @@ public String name() {
}

@Override
protected boolean augmentIfKept(IndexView index, Logger log) {
final var primaryTypeDN = typeAt(0).name();
protected boolean augmentIfKept(IndexView index, Logger log, Map<String, Object> context) {
final var primaryTypeDN = primaryTypeName();

// if we get CustomResource instead of a subclass, ignore the controller since we cannot do anything with it
if (primaryTypeDN.toString() == null || CUSTOM_RESOURCE.equals(primaryTypeDN)
Expand All @@ -53,14 +58,35 @@ protected boolean augmentIfKept(IndexView index, Logger log) {
isCR = crStatus.isCR;
hasNonVoidStatus = crStatus.hasNonVoidStatus;

final ClassInfo primaryCI = index.getClassByName(primaryTypeDN);
holder = new SimpleLoadableResourceHolder<>(primaryCI);

return true;
}

@Override
public String getAssociatedResourceTypeName() {
return holder.getAssociatedResourceTypeName();
}

@Override
public Class<HasMetadata> loadAssociatedClass() {
return holder.loadAssociatedClass();
}

public boolean isCRTargeting() {
return isCR;
}

public boolean hasNonVoidStatus() {
return hasNonVoidStatus;
}

public ResourceTargetingAugmentedClassInfo getAssociatedCustomResourceInfo() {
if (isCR) {
return new ResourceTargetingAugmentedClassInfo(holder.getResourceCI(), name);
}
throw new IllegalStateException("Cannot get associated Custom Resource information because '"
+ name + "' targets " + primaryTypeName().toString() + " primary resource which isn't a Custom Resource.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.quarkiverse.operatorsdk.common;

import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;

import io.fabric8.kubernetes.client.CustomResource;

public class ResourceTargetingAugmentedClassInfo extends SelectiveAugmentedClassInfo
implements LoadableResourceHolder<CustomResource<?, ?>> {
private final LoadableResourceHolder<CustomResource<?, ?>> holder;
private final String reconcilerName;
public static final String EXISTING_CRDS_KEY = "existing-crds-key";

protected ResourceTargetingAugmentedClassInfo(ClassInfo classInfo, String associatedReconcilerName) {
super(classInfo, Constants.CUSTOM_RESOURCE, 2);
this.reconcilerName = associatedReconcilerName;
this.holder = new SimpleLoadableResourceHolder<>(classInfo);
}

@Override
protected boolean augmentIfKept(IndexView index, Logger log, Map<String, Object> context) {
// only keep the information if the associated CRD hasn't already been generated
return Optional.ofNullable(context.get(EXISTING_CRDS_KEY))
.map(value -> {
@SuppressWarnings("unchecked")
Set<String> generated = (Set<String>) value;
return !generated.contains(getAssociatedResourceTypeName());
})
.orElse(true);
}

public String getAssociatedResourceTypeName() {
return holder.getAssociatedResourceTypeName();
}

public Optional<String> getAssociatedReconcilerName() {
return Optional.ofNullable(reconcilerName);
}

public Class<CustomResource<?, ?>> loadAssociatedClass() {
return holder.loadAssociatedClass();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
Expand Down Expand Up @@ -35,7 +36,7 @@ public ClassInfo classInfo() {
return classInfo;
}

boolean keepAugmented(IndexView index, Logger log) {
boolean keepAugmented(IndexView index, Logger log, Map<String, Object> context) {
final var targetClassName = extendedOrImplementedClassName();
final var consideredClassName = classInfo.name();
if (Modifier.isAbstract(classInfo.flags())) {
Expand All @@ -61,7 +62,7 @@ boolean keepAugmented(IndexView index, Logger log) {
}
this.types = typeParameters.toArray(Type[]::new);

return augmentIfKept(index, log);
return augmentIfKept(index, log, context);
}

public List<String> getClassNamesToRegisterForReflection() {
Expand All @@ -82,7 +83,8 @@ protected String extendedOrImplementedClassName() {
return extendedOrImplementedClass.local();
}

protected abstract boolean augmentIfKept(IndexView index, Logger log);
protected abstract boolean augmentIfKept(IndexView index, Logger log,
Map<String, Object> context);

protected CRStatus handlePossibleCR(DotName primaryTypeDN, IndexView index, Logger log) {
// register spec and status for reflection if we're targeting a CustomResource
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.quarkiverse.operatorsdk.common;

import static io.quarkiverse.operatorsdk.common.ClassLoadingUtils.loadClass;

import org.jboss.jandex.ClassInfo;

import io.fabric8.kubernetes.api.model.HasMetadata;

public class SimpleLoadableResourceHolder<T extends HasMetadata> implements LoadableResourceHolder<T> {

private Class<T> clazz;
private final String resourceTypeName;
private final String resourceClassName;
private final ClassInfo resourceCI;

public SimpleLoadableResourceHolder(ClassInfo resourceCI) {
this.resourceTypeName = HasMetadataUtils.getFullResourceName(resourceCI);
this.resourceClassName = resourceCI.name().toString();
this.resourceCI = resourceCI;
}

public String getAssociatedResourceTypeName() {
return resourceTypeName;
}

@SuppressWarnings("unchecked")
public Class<T> loadAssociatedClass() {
if (clazz == null) {
clazz = (Class<T>) loadClass(resourceClassName, HasMetadata.class);
}
return clazz;
}

public ClassInfo getResourceCI() {
return resourceCI;
}
}