diff --git a/CHANGELOG.md b/CHANGELOG.md index 61cb0ae1b83..e82ddb08f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crd-generator/api/pom.xml b/crd-generator/api/pom.xml index 0093914d332..49574461d2f 100644 --- a/crd-generator/api/pom.xml +++ b/crd-generator/api/pom.xml @@ -35,6 +35,11 @@ kubernetes-client-api compile + + + com.fasterxml.jackson.module + jackson-module-jsonSchema + io.fabric8 diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java index 68e266807db..c4feaf5b0a1 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java @@ -16,7 +16,11 @@ 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; @@ -24,6 +28,7 @@ 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; @@ -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; @@ -122,6 +128,9 @@ public abstract class AbstractJsonSchema { 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 COMPLEX_JAVA_TYPES = new HashSet<>(); + static { COMMON_MAPPINGS.put(STRING_REF, STRING_MARKER); COMMON_MAPPINGS.put(DATE_REF, STRING_MARKER); @@ -138,6 +147,10 @@ public abstract class AbstractJsonSchema { 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) { @@ -853,18 +866,67 @@ private T internalFromImpl(String name, TypeRef typeRef, LinkedHashMap 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) * diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java index 1540a775613..5eda53475fe 100644 --- a/crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java +++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java @@ -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; @@ -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 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 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"), @@ -69,21 +73,21 @@ private static Stream 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")); } }