Skip to content

Commit

Permalink
fix(crd-generator): rely upon jackson json schema for java types (#5877
Browse files Browse the repository at this point in the history
…) (#5866)

closes #5866
  • Loading branch information
shawkins authored Apr 23, 2024
1 parent 0f237c0 commit b8eeca4
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 6.13-SNAPSHOT

#### Bugs
* Fix #5866: Addressed cycle in crd generation with Java 19+ and ZonedDateTime

#### Improvements
* Fix #5878: (java-generator) Add implements Editable for extraAnnotations
Expand Down
5 changes: 5 additions & 0 deletions crd-generator/api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
<artifactId>kubernetes-client-api</artifactId>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jsonSchema</artifactId>
</dependency>

<dependency>
<groupId>io.fabric8</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@
package io.fabric8.crd.generator;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema.Items;
import io.fabric8.crd.generator.InternalSchemaSwaps.SwapResult;
import io.fabric8.crd.generator.annotation.SchemaSwap;
import io.fabric8.crd.generator.utils.Types;
import io.fabric8.generator.annotation.ValidationRule;
import io.fabric8.kubernetes.api.model.Duration;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.sundr.builder.internal.functions.TypeAs;
import io.sundr.model.AnnotationRef;
import io.sundr.model.ClassRef;
Expand All @@ -43,6 +48,7 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
Expand Down Expand Up @@ -122,6 +128,9 @@ public abstract class AbstractJsonSchema<T, B> {
public static final String JSON_NODE_TYPE = "com.fasterxml.jackson.databind.JsonNode";
public static final String ANY_TYPE = "io.fabric8.kubernetes.api.model.AnyType";

private static final JsonSchemaGenerator GENERATOR;
private static final Set<String> COMPLEX_JAVA_TYPES = new HashSet<>();

static {
COMMON_MAPPINGS.put(STRING_REF, STRING_MARKER);
COMMON_MAPPINGS.put(DATE_REF, STRING_MARKER);
Expand All @@ -138,6 +147,10 @@ public abstract class AbstractJsonSchema<T, B> {
COMMON_MAPPINGS.put(QUANTITY_REF, INT_OR_STRING_MARKER);
COMMON_MAPPINGS.put(INT_OR_STRING_REF, INT_OR_STRING_MARKER);
COMMON_MAPPINGS.put(DURATION_REF, STRING_MARKER);
ObjectMapper mapper = new ObjectMapper();
// initialize with client defaults
new KubernetesSerialization(mapper, false);
GENERATOR = new JsonSchemaGenerator(mapper);
}

public static String getSchemaTypeFor(TypeRef typeRef) {
Expand Down Expand Up @@ -853,18 +866,67 @@ private T internalFromImpl(String name, TypeRef typeRef, LinkedHashMap<String, S

private T resolveNestedClass(String name, TypeDef def, LinkedHashMap<String, String> visited,
InternalSchemaSwaps schemaSwaps) {
if (visited.put(def.getFullyQualifiedName(), name) != null) {
String fullyQualifiedName = def.getFullyQualifiedName();
T res = resolveJavaClass(fullyQualifiedName);
if (res != null) {
return res;
}
if (visited.put(fullyQualifiedName, name) != null) {
throw new IllegalArgumentException(
"Found a cyclic reference involving the field of type " + def.getFullyQualifiedName() + " starting a field "
"Found a cyclic reference involving the field of type " + fullyQualifiedName + " starting a field "
+ visited.entrySet().stream().map(e -> e.getValue() + " >>\n" + e.getKey()).collect(Collectors.joining(".")) + "."
+ name);
}

T res = internalFromImpl(def, visited, schemaSwaps);
visited.remove(def.getFullyQualifiedName());
res = internalFromImpl(def, visited, schemaSwaps);
visited.remove(fullyQualifiedName);
return res;
}

private T resolveJavaClass(String fullyQualifiedName) {
if ((!fullyQualifiedName.startsWith("java.") && !fullyQualifiedName.startsWith("javax."))
|| COMPLEX_JAVA_TYPES.contains(fullyQualifiedName)) {
return null;
}
String mapping = null;
boolean array = false;
try {
Class<?> clazz = Class.forName(fullyQualifiedName);
JsonSchema schema = GENERATOR.generateSchema(clazz);
if (schema.isArraySchema()) {
Items items = schema.asArraySchema().getItems();
if (items.isSingleItems()) {
array = true;
schema = items.asSingleItems().getSchema();
}
}
if (schema.isIntegerSchema()) {
mapping = INTEGER_MARKER;
} else if (schema.isNumberSchema()) {
mapping = NUMBER_MARKER;
} else if (schema.isBooleanSchema()) {
mapping = BOOLEAN_MARKER;
} else if (schema.isStringSchema()) {
mapping = STRING_MARKER;
}
} catch (Exception e) {
LOGGER.debug(
"Something went wrong with detecting java type schema for {}, will use full introspection instead",
fullyQualifiedName, e);
}
// cache the result for subsequent calls
if (mapping != null) {
if (array) {
return arrayLikeProperty(singleProperty(mapping));
}
COMMON_MAPPINGS.put(TypeDef.forName(fullyQualifiedName).toReference(), mapping);
return singleProperty(mapping);
}

COMPLEX_JAVA_TYPES.add(fullyQualifiedName);
return null;
}

/**
* Builds the schema for specifically handled property types (e.g. intOrString properties)
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
package io.fabric8.crd.generator.types;

import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
import io.fabric8.kubernetes.client.utils.Serialization;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

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

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -40,27 +42,29 @@ void setUp() {
@ParameterizedTest(name = "{0} type maps to {1}")
@MethodSource("targetTypeCases")
void targetType(String propertyName, String expectedType) {
assertThat(crd.getSpec().getVersions().iterator().next().getSchema().getOpenAPIV3Schema().getProperties())
.extracting(schema -> schema.get("spec").getProperties())
Map<String, JSONSchemaProps> properties = crd.getSpec().getVersions().iterator().next().getSchema().getOpenAPIV3Schema()
.getProperties().get("spec").getProperties();
assertThat(properties)
.withFailMessage("Expected %s to be %s, but was %s", propertyName, expectedType, properties.get(propertyName).getType())
.returns(expectedType, specProps -> specProps.get(propertyName).getType());
}

private static Stream<Arguments> targetTypeCases() {
return Stream.of(
Arguments.of("date", "string"),
Arguments.of("localDate", "object"), // to review
Arguments.of("localDateTime", "object"), // to review
Arguments.of("zonedDateTime", "object"), // to review
Arguments.of("offsetDateTime", "object"), // to review
Arguments.of("offsetTime", "object"), // to review
Arguments.of("yearMonth", "object"), // to review
Arguments.of("monthDay", "object"), // to review
Arguments.of("instant", "object"), // to review
Arguments.of("duration", "object"), // to review
Arguments.of("period", "object"), // to review
Arguments.of("timestamp", "object"), // to review
Arguments.of("localDate", "array"), // to review
Arguments.of("localDateTime", "array"), // to review
Arguments.of("zonedDateTime", "number"), // to review
Arguments.of("offsetDateTime", "number"), // to review
Arguments.of("offsetTime", "array"), // to review
Arguments.of("yearMonth", "array"), // to review
Arguments.of("monthDay", "array"), // to review
Arguments.of("instant", "number"), // to review
Arguments.of("duration", "integer"), // to review
Arguments.of("period", "string"),
Arguments.of("timestamp", "integer"), // to review
// Arguments.of("aShort", "integer"), // TODO: Not even present in the CRD
Arguments.of("aShortObj", "object"), // to review
Arguments.of("aShortObj", "integer"),
Arguments.of("aInt", "integer"),
Arguments.of("aIntegerObj", "integer"),
Arguments.of("aLong", "integer"),
Expand All @@ -69,21 +73,21 @@ private static Stream<Arguments> targetTypeCases() {
Arguments.of("aDoubleObj", "number"),
Arguments.of("aFloat", "number"),
Arguments.of("aFloatObj", "number"),
Arguments.of("aNumber", "object"), // to review
Arguments.of("aBigInteger", "object"), // to review
Arguments.of("aBigDecimal", "object"), // to review
Arguments.of("aNumber", "number"),
Arguments.of("aBigInteger", "integer"),
Arguments.of("aBigDecimal", "number"),
Arguments.of("aBoolean", "boolean"),
Arguments.of("aBooleanObj", "boolean"),
// Arguments.of("aChar", "string"), // TODO: Not even present in the CRD
Arguments.of("aCharacterObj", "object"), // to review
Arguments.of("aCharacterObj", "string"),
Arguments.of("aCharArray", "array"),
Arguments.of("aCharSequence", "object"), // to review
Arguments.of("aCharSequence", "string"),
Arguments.of("aString", "string"),
Arguments.of("aStringArray", "array"),
// Arguments.of("aByte", "?"), // TODO: Not even present in the CRD
Arguments.of("aByteObj", "object"), // to review
Arguments.of("aByteObj", "integer"),
Arguments.of("aByteArray", "array"), // to review, should be string (base64)
Arguments.of("uuid", "object")); // to review, should be string
Arguments.of("uuid", "string"));
}

}

0 comments on commit b8eeca4

Please sign in to comment.