Skip to content

Commit b8eeca4

Browse files
authored
fix(crd-generator): rely upon jackson json schema for java types (#5877) (#5866)
closes #5866
1 parent 0f237c0 commit b8eeca4

File tree

4 files changed

+97
-25
lines changed

4 files changed

+97
-25
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### 6.13-SNAPSHOT
44

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

78
#### Improvements
89
* Fix #5878: (java-generator) Add implements Editable for extraAnnotations

crd-generator/api/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
<artifactId>kubernetes-client-api</artifactId>
3636
<scope>compile</scope>
3737
</dependency>
38+
39+
<dependency>
40+
<groupId>com.fasterxml.jackson.module</groupId>
41+
<artifactId>jackson-module-jsonSchema</artifactId>
42+
</dependency>
3843

3944
<dependency>
4045
<groupId>io.fabric8</groupId>

crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java

+66-4
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@
1616
package io.fabric8.crd.generator;
1717

1818
import com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
1920
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
21+
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
22+
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
23+
import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema.Items;
2024
import io.fabric8.crd.generator.InternalSchemaSwaps.SwapResult;
2125
import io.fabric8.crd.generator.annotation.SchemaSwap;
2226
import io.fabric8.crd.generator.utils.Types;
2327
import io.fabric8.generator.annotation.ValidationRule;
2428
import io.fabric8.kubernetes.api.model.Duration;
2529
import io.fabric8.kubernetes.api.model.IntOrString;
2630
import io.fabric8.kubernetes.api.model.Quantity;
31+
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
2732
import io.sundr.builder.internal.functions.TypeAs;
2833
import io.sundr.model.AnnotationRef;
2934
import io.sundr.model.ClassRef;
@@ -43,6 +48,7 @@
4348
import java.util.Collections;
4449
import java.util.Date;
4550
import java.util.HashMap;
51+
import java.util.HashSet;
4652
import java.util.LinkedHashMap;
4753
import java.util.LinkedHashSet;
4854
import java.util.LinkedList;
@@ -122,6 +128,9 @@ public abstract class AbstractJsonSchema<T, B> {
122128
public static final String JSON_NODE_TYPE = "com.fasterxml.jackson.databind.JsonNode";
123129
public static final String ANY_TYPE = "io.fabric8.kubernetes.api.model.AnyType";
124130

131+
private static final JsonSchemaGenerator GENERATOR;
132+
private static final Set<String> COMPLEX_JAVA_TYPES = new HashSet<>();
133+
125134
static {
126135
COMMON_MAPPINGS.put(STRING_REF, STRING_MARKER);
127136
COMMON_MAPPINGS.put(DATE_REF, STRING_MARKER);
@@ -138,6 +147,10 @@ public abstract class AbstractJsonSchema<T, B> {
138147
COMMON_MAPPINGS.put(QUANTITY_REF, INT_OR_STRING_MARKER);
139148
COMMON_MAPPINGS.put(INT_OR_STRING_REF, INT_OR_STRING_MARKER);
140149
COMMON_MAPPINGS.put(DURATION_REF, STRING_MARKER);
150+
ObjectMapper mapper = new ObjectMapper();
151+
// initialize with client defaults
152+
new KubernetesSerialization(mapper, false);
153+
GENERATOR = new JsonSchemaGenerator(mapper);
141154
}
142155

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

854867
private T resolveNestedClass(String name, TypeDef def, LinkedHashMap<String, String> visited,
855868
InternalSchemaSwaps schemaSwaps) {
856-
if (visited.put(def.getFullyQualifiedName(), name) != null) {
869+
String fullyQualifiedName = def.getFullyQualifiedName();
870+
T res = resolveJavaClass(fullyQualifiedName);
871+
if (res != null) {
872+
return res;
873+
}
874+
if (visited.put(fullyQualifiedName, name) != null) {
857875
throw new IllegalArgumentException(
858-
"Found a cyclic reference involving the field of type " + def.getFullyQualifiedName() + " starting a field "
876+
"Found a cyclic reference involving the field of type " + fullyQualifiedName + " starting a field "
859877
+ visited.entrySet().stream().map(e -> e.getValue() + " >>\n" + e.getKey()).collect(Collectors.joining(".")) + "."
860878
+ name);
861879
}
862880

863-
T res = internalFromImpl(def, visited, schemaSwaps);
864-
visited.remove(def.getFullyQualifiedName());
881+
res = internalFromImpl(def, visited, schemaSwaps);
882+
visited.remove(fullyQualifiedName);
865883
return res;
866884
}
867885

886+
private T resolveJavaClass(String fullyQualifiedName) {
887+
if ((!fullyQualifiedName.startsWith("java.") && !fullyQualifiedName.startsWith("javax."))
888+
|| COMPLEX_JAVA_TYPES.contains(fullyQualifiedName)) {
889+
return null;
890+
}
891+
String mapping = null;
892+
boolean array = false;
893+
try {
894+
Class<?> clazz = Class.forName(fullyQualifiedName);
895+
JsonSchema schema = GENERATOR.generateSchema(clazz);
896+
if (schema.isArraySchema()) {
897+
Items items = schema.asArraySchema().getItems();
898+
if (items.isSingleItems()) {
899+
array = true;
900+
schema = items.asSingleItems().getSchema();
901+
}
902+
}
903+
if (schema.isIntegerSchema()) {
904+
mapping = INTEGER_MARKER;
905+
} else if (schema.isNumberSchema()) {
906+
mapping = NUMBER_MARKER;
907+
} else if (schema.isBooleanSchema()) {
908+
mapping = BOOLEAN_MARKER;
909+
} else if (schema.isStringSchema()) {
910+
mapping = STRING_MARKER;
911+
}
912+
} catch (Exception e) {
913+
LOGGER.debug(
914+
"Something went wrong with detecting java type schema for {}, will use full introspection instead",
915+
fullyQualifiedName, e);
916+
}
917+
// cache the result for subsequent calls
918+
if (mapping != null) {
919+
if (array) {
920+
return arrayLikeProperty(singleProperty(mapping));
921+
}
922+
COMMON_MAPPINGS.put(TypeDef.forName(fullyQualifiedName).toReference(), mapping);
923+
return singleProperty(mapping);
924+
}
925+
926+
COMPLEX_JAVA_TYPES.add(fullyQualifiedName);
927+
return null;
928+
}
929+
868930
/**
869931
* Builds the schema for specifically handled property types (e.g. intOrString properties)
870932
*

crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java

+25-21
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
package io.fabric8.crd.generator.types;
1717

1818
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
19+
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
1920
import io.fabric8.kubernetes.client.utils.Serialization;
2021
import org.junit.jupiter.api.BeforeEach;
2122
import org.junit.jupiter.params.ParameterizedTest;
2223
import org.junit.jupiter.params.provider.Arguments;
2324
import org.junit.jupiter.params.provider.MethodSource;
2425

26+
import java.util.Map;
2527
import java.util.stream.Stream;
2628

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

4852
private static Stream<Arguments> targetTypeCases() {
4953
return Stream.of(
5054
Arguments.of("date", "string"),
51-
Arguments.of("localDate", "object"), // to review
52-
Arguments.of("localDateTime", "object"), // to review
53-
Arguments.of("zonedDateTime", "object"), // to review
54-
Arguments.of("offsetDateTime", "object"), // to review
55-
Arguments.of("offsetTime", "object"), // to review
56-
Arguments.of("yearMonth", "object"), // to review
57-
Arguments.of("monthDay", "object"), // to review
58-
Arguments.of("instant", "object"), // to review
59-
Arguments.of("duration", "object"), // to review
60-
Arguments.of("period", "object"), // to review
61-
Arguments.of("timestamp", "object"), // to review
55+
Arguments.of("localDate", "array"), // to review
56+
Arguments.of("localDateTime", "array"), // to review
57+
Arguments.of("zonedDateTime", "number"), // to review
58+
Arguments.of("offsetDateTime", "number"), // to review
59+
Arguments.of("offsetTime", "array"), // to review
60+
Arguments.of("yearMonth", "array"), // to review
61+
Arguments.of("monthDay", "array"), // to review
62+
Arguments.of("instant", "number"), // to review
63+
Arguments.of("duration", "integer"), // to review
64+
Arguments.of("period", "string"),
65+
Arguments.of("timestamp", "integer"), // to review
6266
// Arguments.of("aShort", "integer"), // TODO: Not even present in the CRD
63-
Arguments.of("aShortObj", "object"), // to review
67+
Arguments.of("aShortObj", "integer"),
6468
Arguments.of("aInt", "integer"),
6569
Arguments.of("aIntegerObj", "integer"),
6670
Arguments.of("aLong", "integer"),
@@ -69,21 +73,21 @@ private static Stream<Arguments> targetTypeCases() {
6973
Arguments.of("aDoubleObj", "number"),
7074
Arguments.of("aFloat", "number"),
7175
Arguments.of("aFloatObj", "number"),
72-
Arguments.of("aNumber", "object"), // to review
73-
Arguments.of("aBigInteger", "object"), // to review
74-
Arguments.of("aBigDecimal", "object"), // to review
76+
Arguments.of("aNumber", "number"),
77+
Arguments.of("aBigInteger", "integer"),
78+
Arguments.of("aBigDecimal", "number"),
7579
Arguments.of("aBoolean", "boolean"),
7680
Arguments.of("aBooleanObj", "boolean"),
7781
// Arguments.of("aChar", "string"), // TODO: Not even present in the CRD
78-
Arguments.of("aCharacterObj", "object"), // to review
82+
Arguments.of("aCharacterObj", "string"),
7983
Arguments.of("aCharArray", "array"),
80-
Arguments.of("aCharSequence", "object"), // to review
84+
Arguments.of("aCharSequence", "string"),
8185
Arguments.of("aString", "string"),
8286
Arguments.of("aStringArray", "array"),
8387
// Arguments.of("aByte", "?"), // TODO: Not even present in the CRD
84-
Arguments.of("aByteObj", "object"), // to review
88+
Arguments.of("aByteObj", "integer"),
8589
Arguments.of("aByteArray", "array"), // to review, should be string (base64)
86-
Arguments.of("uuid", "object")); // to review, should be string
90+
Arguments.of("uuid", "string"));
8791
}
8892

8993
}

0 commit comments

Comments
 (0)