From 039b0c6bf5e5041393bf415a0f43dcaf964fa318 Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Sun, 21 Apr 2024 12:33:02 -0400 Subject: [PATCH] starting on v7 crd generator this removes the usage of sundrio and leverages many features already provided by jackson Signed-off-by: Steve Hawkins --- crd-generator/api/pom.xml | 14 - .../AbstractCustomResourceHandler.java | 136 +-- .../crd/generator/AbstractJsonSchema.java | 1081 ++++++----------- .../crd/generator/CRDGenerationInfo.java | 5 +- .../fabric8/crd/generator/CRDGenerator.java | 3 +- .../io/fabric8/crd/generator/CRDInfo.java | 9 +- .../crd/generator/CustomResourceInfo.java | 49 +- .../crd/generator/InternalSchemaSwaps.java | 50 +- .../crd/generator/ResolvingContext.java | 127 ++ .../io/fabric8/crd/generator/utils/Types.java | 242 +--- .../generator/v1/CustomResourceHandler.java | 31 +- .../fabric8/crd/generator/v1/JsonSchema.java | 114 +- .../v1beta1/CustomResourceHandler.java | 32 +- .../crd/generator/v1beta1/JsonSchema.java | 116 +- .../AdditionalPrinterColumnDetector.java | 29 - .../AnnotatedMultiPropertyPathDetector.java | 110 -- .../AnnotatedPropertyPathDetector.java | 114 -- .../visitor/ClassDependenciesVisitor.java | 109 -- .../visitor/LabelSelectorPathDetector.java | 29 - .../visitor/SpecReplicasPathDetector.java | 29 - .../visitor/StatusReplicasPathDetector.java | 30 - .../io/fabric8/crd/alt/LabelSelector.java | 27 - .../java/io/fabric8/crd/alt/SpecReplicas.java | 27 - .../io/fabric8/crd/alt/StatusReplicas.java | 27 - .../crd/example/cyclic/CyclicListSpec.java | 3 + .../crd/example/cyclic/CyclicSpec.java | 3 + .../crd/example/cyclic/CyclicStatus.java | 3 + .../io/fabric8/crd/example/cyclic/Ref.java | 3 + .../fabric8/crd/example/cyclic/RefList.java | 3 + .../CollectionCyclicSchemaSwap.java | 10 +- .../example/extraction/CyclicSchemaSwap.java | 12 +- .../extraction/DeeplyNestedSchemaSwaps.java | 22 +- .../example/extraction/ExtractionSpec.java | 4 +- .../example/extraction/NestedSchemaSwap.java | 8 +- .../example/extraction/SchemaSwapSpec.java | 14 +- .../crd/example/inherited/ChildSpec.java | 3 + .../k8svalidation/K8sValidationSpec.java | 6 + .../k8svalidation/K8sValidationStatus.java | 3 + .../crd/example/map/ContainingMaps.java | 2 + .../crd/example/map/ContainingMapsSpec.java | 3 + .../fabric8/crd/example/person/Address.java | 10 +- .../io/fabric8/crd/example/person/Person.java | 14 +- .../crd/example/webserver/WebServerSpec.java | 4 +- .../example/webserver/WebServerStatus.java | 4 +- .../example/webserver/WebServerWithSpec.java | 3 + .../WebServerWithStatusProperty.java | 4 +- .../crd/generator/CRDGeneratorAssertions.java | 1 + .../crd/generator/CRDGeneratorTest.java | 46 +- .../crd/generator/utils/TypesTest.java | 72 +- .../crd/generator/v1/JsonSchemaTest.java | 226 +++- .../SpecReplicasPathTest.java} | 33 +- crd-generator/apt/pom.xml | 51 - .../CustomResourceAnnotationProcessor.java | 247 ---- .../javax.annotation.processing.Processor | 1 - crd-generator/pom.xml | 13 - crd-generator/test/pom.xml | 4 +- doc/CRD-generator.md | 13 +- .../kubernetes/client/CustomResource.java | 10 +- .../client/utils/KubernetesSerialization.java | 2 +- .../kubernetes/api/model/Duration.java | 16 +- kubernetes-tests/pom.xml | 5 - pom.xml | 5 - 62 files changed, 1054 insertions(+), 2372 deletions(-) create mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/ResolvingContext.java delete mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AdditionalPrinterColumnDetector.java delete mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AnnotatedMultiPropertyPathDetector.java delete mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AnnotatedPropertyPathDetector.java delete mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/ClassDependenciesVisitor.java delete mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/LabelSelectorPathDetector.java delete mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/SpecReplicasPathDetector.java delete mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/StatusReplicasPathDetector.java delete mode 100644 crd-generator/api/src/test/java/io/fabric8/crd/alt/LabelSelector.java delete mode 100644 crd-generator/api/src/test/java/io/fabric8/crd/alt/SpecReplicas.java delete mode 100644 crd-generator/api/src/test/java/io/fabric8/crd/alt/StatusReplicas.java rename crd-generator/api/src/test/java/io/fabric8/crd/generator/{visitor/SpecReplicasPathDetectorTest.java => v1/SpecReplicasPathTest.java} (50%) delete mode 100644 crd-generator/apt/pom.xml delete mode 100644 crd-generator/apt/src/main/java/io/fabric8/crd/generator/apt/CustomResourceAnnotationProcessor.java delete mode 100644 crd-generator/apt/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/crd-generator/api/pom.xml b/crd-generator/api/pom.xml index 49574461d2f..21b12d7642f 100644 --- a/crd-generator/api/pom.xml +++ b/crd-generator/api/pom.xml @@ -51,20 +51,6 @@ kubernetes-model-common - - io.sundr - sundr-adapter-reflect - ${sundrio.version} - compile - - - - io.sundr - builder-annotations - compile - - - org.junit.jupiter diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractCustomResourceHandler.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractCustomResourceHandler.java index e6f63572cfd..f7f166b2c7e 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractCustomResourceHandler.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractCustomResourceHandler.java @@ -15,26 +15,12 @@ */ package io.fabric8.crd.generator; +import io.fabric8.crd.generator.AbstractJsonSchema.AnnotationMetadata; +import io.fabric8.crd.generator.annotation.PrinterColumn; import io.fabric8.crd.generator.decorator.Decorator; -import io.fabric8.crd.generator.visitor.*; import io.fabric8.kubernetes.client.utils.Utils; -import io.sundr.builder.Visitor; -import io.sundr.model.AnnotationRef; -import io.sundr.model.Property; -import io.sundr.model.TypeDef; -import io.sundr.model.TypeDefBuilder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; /** * This class encapsulates the common behavior between v1beta1 and v1 CRD generation logic. The @@ -43,110 +29,31 @@ public abstract class AbstractCustomResourceHandler { protected final Resources resources; - private final boolean parallel; - protected AbstractCustomResourceHandler(Resources resources, boolean parallel) { + protected AbstractCustomResourceHandler(Resources resources) { this.resources = resources; - this.parallel = parallel; } - public void handle(CustomResourceInfo config) { - final String name = config.crdName(); - final String version = config.version(); + public abstract void handle(CustomResourceInfo config); - TypeDef def = config.definition(); - - SpecReplicasPathDetector specReplicasPathDetector = new SpecReplicasPathDetector(); - StatusReplicasPathDetector statusReplicasPathDetector = new StatusReplicasPathDetector(); - LabelSelectorPathDetector labelSelectorPathDetector = new LabelSelectorPathDetector(); - AdditionalPrinterColumnDetector additionalPrinterColumnDetector = new AdditionalPrinterColumnDetector(); - - ClassDependenciesVisitor traversedClassesVisitor = new ClassDependenciesVisitor(config.crClassName(), name); - - List> visitors = new ArrayList<>(); - if (config.specClassName().isPresent()) { - visitors.add(specReplicasPathDetector); - } - if (config.statusClassName().isPresent()) { - visitors.add(statusReplicasPathDetector); - } - visitors.add(labelSelectorPathDetector); - visitors.add(additionalPrinterColumnDetector); - visitors.add(traversedClassesVisitor); - - visitTypeDef(def, visitors); - - addDecorators(config, def, specReplicasPathDetector.getPath(), - statusReplicasPathDetector.getPath(), labelSelectorPathDetector.getPath()); - - Map additionalPrinterColumns = new HashMap<>(additionalPrinterColumnDetector.getProperties()); + protected void handlePrinterColumns(String name, String version, Map additionalPrinterColumns) { additionalPrinterColumns.forEach((path, property) -> { - Map parameters = property.getAnnotations().stream() - .filter(a -> a.getClassRef().getName().equals("PrinterColumn")).map(AnnotationRef::getParameters) - .findFirst().orElse(Collections.emptyMap()); - String type = AbstractJsonSchema.getSchemaTypeFor(property.getTypeRef()); - String column = (String) parameters.get("name"); + PrinterColumn printerColumn = ((PrinterColumn)property.annotation); + String column = printerColumn.name(); if (Utils.isNullOrEmpty(column)) { - column = property.getName().toUpperCase(); + column = path.substring(path.lastIndexOf(".")); } - String description = property.getComments().stream().filter(l -> !l.trim().startsWith("@")) - .collect(Collectors.joining(" ")).trim(); - String format = (String) parameters.get("format"); - int priority = (int) parameters.getOrDefault("priority", 0); + String format = printerColumn.format(); + int priority = printerColumn.priority(); + + // TODO: add description to the annotation? The previous logic considered the comments, which are not available here + String description = property.description; resources.decorate( - getPrinterColumnDecorator(name, version, path, type, column, description, format, priority)); + getPrinterColumnDecorator(name, version, path, property.type, column, description, format, priority)); }); } - private TypeDef visitTypeDef(TypeDef def, List> visitors) { - if (visitors.isEmpty()) { - return def; - } - if (parallel) { - return visitTypeDefInParallel(def, visitors); - } else { - return visitTypeDefSequentially(def, visitors); - } - } - - private TypeDef visitTypeDefSequentially(TypeDef def, List> visitors) { - TypeDefBuilder builder = new TypeDefBuilder(def); - for (Visitor visitor : visitors) { - builder.accept(visitor); - } - return builder.build(); - } - - private TypeDef visitTypeDefInParallel(TypeDef def, List> visitors) { - final ExecutorService executorService = Executors.newFixedThreadPool( - Math.min(visitors.size(), Runtime.getRuntime().availableProcessors())); - try { - List> futures = new ArrayList<>(); - for (Visitor visitor : visitors) { - futures.add(CompletableFuture.runAsync(() -> { - // in this case we're not building a new typedef, - // instead we just need to traverse the object graph. - TypeDefBuilder builder = new TypeDefBuilder(def); - builder.accept(visitor); - }, executorService)); - } - try { - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); - } catch (InterruptedException interruptedException) { - Thread.currentThread().interrupt(); - } catch (ExecutionException ex) { - if (ex.getCause() instanceof RuntimeException) { - throw (RuntimeException) ex.getCause(); - } - throw new RuntimeException(ex.getCause()); - } - } finally { - executorService.shutdown(); - } - return def; - } - /** * Provides the decorator implementation associated with the CRD generation version. * @@ -162,19 +69,4 @@ private TypeDef visitTypeDefInParallel(TypeDef def, List protected abstract Decorator getPrinterColumnDecorator(String name, String version, String path, String type, String column, String description, String format, int priority); - /** - * Adds all the necessary decorators to build the specific CRD version. For optional paths, see - * https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#customresourcesubresourcescale-v1-apiextensions-k8s-io - * These paths - * - * @param config the gathered {@link CustomResourceInfo} used as basis for the CRD generation - * @param def the {@link TypeDef} associated with the {@link io.fabric8.kubernetes.client.CustomResource} from which the CRD - * is generated - * @param specReplicasPath an optionally detected path of field defining spec replicas - * @param statusReplicasPath an optionally detected path of field defining status replicas - * @param labelSelectorPath an optionally detected path of field defining `status.selector` - */ - protected abstract void addDecorators(CustomResourceInfo config, TypeDef def, - Optional specReplicasPath, Optional statusReplicasPath, - Optional labelSelectorPath); } 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 c4feaf5b0a1..f5854f7a717 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 @@ -15,639 +15,467 @@ */ package io.fabric8.crd.generator; +import com.fasterxml.jackson.annotation.JsonFormat.Value; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; 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 com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema.SchemaAdditionalProperties; +import com.fasterxml.jackson.module.jsonSchema.types.ReferenceSchema; +import com.fasterxml.jackson.module.jsonSchema.types.StringSchema; +import com.fasterxml.jackson.module.jsonSchema.types.ValueTypeSchema; import io.fabric8.crd.generator.InternalSchemaSwaps.SwapResult; +import io.fabric8.crd.generator.ResolvingContext.GeneratorObjectSchema; +import io.fabric8.crd.generator.annotation.PreserveUnknownFields; +import io.fabric8.crd.generator.annotation.PrinterColumn; +import io.fabric8.crd.generator.annotation.SchemaFrom; import io.fabric8.crd.generator.annotation.SchemaSwap; -import io.fabric8.crd.generator.utils.Types; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Max; +import io.fabric8.generator.annotation.Min; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.generator.annotation.Pattern; +import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import io.fabric8.kubernetes.api.model.Duration; +import io.fabric8.generator.annotation.ValidationRules; 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; -import io.sundr.model.Method; -import io.sundr.model.PrimitiveRefBuilder; -import io.sundr.model.Property; -import io.sundr.model.TypeDef; -import io.sundr.model.TypeRef; -import io.sundr.model.functions.GetDefinition; -import io.sundr.utils.Strings; +import io.fabric8.kubernetes.client.utils.Utils; +import io.fabric8.kubernetes.model.annotation.LabelSelector; +import io.fabric8.kubernetes.model.annotation.SpecReplicas; +import io.fabric8.kubernetes.model.annotation.StatusReplicas; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; 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; import java.util.List; import java.util.Map; -import java.util.Objects; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; -import static io.sundr.model.utils.Types.BOOLEAN_REF; -import static io.sundr.model.utils.Types.DOUBLE_REF; -import static io.sundr.model.utils.Types.FLOAT_REF; -import static io.sundr.model.utils.Types.INT_REF; -import static io.sundr.model.utils.Types.LONG_REF; -import static io.sundr.model.utils.Types.STRING_REF; -import static io.sundr.model.utils.Types.VOID; +import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER; +import static java.util.Optional.ofNullable; /** * Encapsulates the common logic supporting OpenAPI schema generation for CRD generation. * * @param the concrete type of the generated JSON Schema * @param the concrete type of the JSON Schema builder + * @param the concrete type of the validation rule */ -public abstract class AbstractJsonSchema { +public abstract class AbstractJsonSchema { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJsonSchema.class); - protected static final TypeDef OBJECT = TypeDef.forName(Object.class.getName()); - protected static final TypeDef QUANTITY = TypeDef.forName(Quantity.class.getName()); - protected static final TypeDef DURATION = TypeDef.forName(Duration.class.getName()); - protected static final TypeDef INT_OR_STRING = TypeDef.forName(IntOrString.class.getName()); - - protected static final TypeRef OBJECT_REF = OBJECT.toReference(); - protected static final TypeRef QUANTITY_REF = QUANTITY.toReference(); - protected static final TypeRef DURATION_REF = DURATION.toReference(); - protected static final TypeRef INT_OR_STRING_REF = INT_OR_STRING.toReference(); - - protected static final TypeDef DATE = TypeDef.forName(Date.class.getName()); - protected static final TypeRef DATE_REF = DATE.toReference(); - - private static final String VALUE = "value"; - - private static final String INT_OR_STRING_MARKER = "int_or_string"; - private static final String STRING_MARKER = "string"; - private static final String INTEGER_MARKER = "integer"; - private static final String NUMBER_MARKER = "number"; - private static final String BOOLEAN_MARKER = "boolean"; - - protected static final TypeRef P_INT_REF = new PrimitiveRefBuilder().withName("int").build(); - protected static final TypeRef P_LONG_REF = new PrimitiveRefBuilder().withName("long").build(); - protected static final TypeRef P_FLOAT_REF = new PrimitiveRefBuilder().withName("float").build(); - protected static final TypeRef P_DOUBLE_REF = new PrimitiveRefBuilder().withName("double").build(); - protected static final TypeRef P_BOOLEAN_REF = new PrimitiveRefBuilder().withName(BOOLEAN_MARKER) - .build(); - - private static final Map COMMON_MAPPINGS = new HashMap<>(); - public static final String ANNOTATION_JSON_PROPERTY = "com.fasterxml.jackson.annotation.JsonProperty"; - public static final String ANNOTATION_JSON_PROPERTY_DESCRIPTION = "com.fasterxml.jackson.annotation.JsonPropertyDescription"; - public static final String ANNOTATION_JSON_IGNORE = "com.fasterxml.jackson.annotation.JsonIgnore"; - public static final String ANNOTATION_JSON_ANY_GETTER = "com.fasterxml.jackson.annotation.JsonAnyGetter"; - public static final String ANNOTATION_JSON_ANY_SETTER = "com.fasterxml.jackson.annotation.JsonAnySetter"; - public static final String ANNOTATION_DEFAULT = "io.fabric8.generator.annotation.Default"; - public static final String ANNOTATION_MIN = "io.fabric8.generator.annotation.Min"; - public static final String ANNOTATION_MAX = "io.fabric8.generator.annotation.Max"; - public static final String ANNOTATION_PATTERN = "io.fabric8.generator.annotation.Pattern"; - public static final String ANNOTATION_NULLABLE = "io.fabric8.generator.annotation.Nullable"; - public static final String ANNOTATION_REQUIRED = "io.fabric8.generator.annotation.Required"; - public static final String ANNOTATION_SCHEMA_FROM = "io.fabric8.crd.generator.annotation.SchemaFrom"; - public static final String ANNOTATION_PERSERVE_UNKNOWN_FIELDS = "io.fabric8.crd.generator.annotation.PreserveUnknownFields"; - public static final String ANNOTATION_SCHEMA_SWAP = "io.fabric8.crd.generator.annotation.SchemaSwap"; - public static final String ANNOTATION_SCHEMA_SWAPS = "io.fabric8.crd.generator.annotation.SchemaSwaps"; - public static final String ANNOTATION_VALIDATION_RULE = "io.fabric8.generator.annotation.ValidationRule"; - public static final String ANNOTATION_VALIDATION_RULES = "io.fabric8.generator.annotation.ValidationRules"; - - 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); - COMMON_MAPPINGS.put(INT_REF, INTEGER_MARKER); - COMMON_MAPPINGS.put(P_INT_REF, INTEGER_MARKER); - COMMON_MAPPINGS.put(LONG_REF, INTEGER_MARKER); - COMMON_MAPPINGS.put(P_LONG_REF, INTEGER_MARKER); - COMMON_MAPPINGS.put(FLOAT_REF, NUMBER_MARKER); - COMMON_MAPPINGS.put(P_FLOAT_REF, NUMBER_MARKER); - COMMON_MAPPINGS.put(DOUBLE_REF, NUMBER_MARKER); - COMMON_MAPPINGS.put(P_DOUBLE_REF, NUMBER_MARKER); - COMMON_MAPPINGS.put(BOOLEAN_REF, BOOLEAN_MARKER); - COMMON_MAPPINGS.put(P_BOOLEAN_REF, BOOLEAN_MARKER); - 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) { - String type = COMMON_MAPPINGS.get(typeRef); - if (type == null && typeRef instanceof ClassRef) { // Handle complex types - ClassRef classRef = (ClassRef) typeRef; - TypeDef def = Types.typeDefFrom(classRef); - type = def.isEnum() ? STRING_MARKER : "object"; - } - return type; - } + private ResolvingContext resolvingContext; + private T root; - protected static class SchemaPropsOptions { - final String defaultValue; - final Double min; - final Double max; - final String pattern; - final boolean nullable; - final boolean required; - final boolean preserveUnknownFields; - final List validationRules; - - SchemaPropsOptions() { - defaultValue = null; - min = null; - max = null; - pattern = null; - nullable = false; - required = false; - preserveUnknownFields = false; - validationRules = null; - } - - public SchemaPropsOptions(String defaultValue, Double min, Double max, String pattern, - List validationRules, - boolean nullable, boolean required, boolean preserveUnknownFields) { - this.defaultValue = defaultValue; - this.min = min; - this.max = max; - this.pattern = pattern; - this.nullable = nullable; - this.required = required; - this.preserveUnknownFields = preserveUnknownFields; - this.validationRules = validationRules; - } + public static class AnnotationMetadata { + public final Annotation annotation; + public final String description; + public final String type; - public Optional getDefault() { - return Optional.ofNullable(defaultValue); + public AnnotationMetadata(Annotation annotation, String description, String type) { + this.annotation = annotation; + this.description = description; + this.type = type; } + } - public Optional getMin() { - return Optional.ofNullable(min); - } + private Map, LinkedHashMap> pathMetadata = new HashMap<>(); - public Optional getMax() { - return Optional.ofNullable(max); - } + public AbstractJsonSchema(ResolvingContext resolvingContext, Class def) { + this.resolvingContext = resolvingContext; + // TODO: could make this configurable, and could stop looking for single valued ones - or warn + Stream.of(SpecReplicas.class, StatusReplicas.class, LabelSelector.class, PrinterColumn.class) + .forEach(clazz -> pathMetadata.put(clazz, new LinkedHashMap<>())); - public Optional getPattern() { - return Optional.ofNullable(pattern); - } + this.root = resolveRoot(def); + } - public boolean isNullable() { - return nullable; - } + public T getSchema() { + return root; + } - public boolean getRequired() { - return required; - } + public Optional getSinglePath(Class clazz) { + return ofNullable(pathMetadata.get(clazz)).flatMap(m -> m.keySet().stream().findFirst()); + } - public boolean isPreserveUnknownFields() { - return preserveUnknownFields; - } + public Map getAllPaths(Class clazz) { + return ofNullable(pathMetadata.get(clazz)).orElse(new LinkedHashMap<>()); + } - public List getValidationRules() { - return Optional.ofNullable(validationRules) - .orElseGet(Collections::emptyList); + protected List mapValidationRules(List validationRules) { + if (validationRules == null) { + return Collections.emptyList(); } + return validationRules.stream() + .map(this::mapValidationRule) + .collect(Collectors.toList()); } /** - * Creates the JSON schema for the particular {@link TypeDef}. This is template method where + * Creates the JSON schema for the class. This is template method where * sub-classes are supposed to provide specific implementations of abstract methods. * * @param definition The definition. * @param ignore a potentially empty list of property names to ignore while generating the schema * @return The schema. */ - protected T internalFrom(TypeDef definition, String... ignore) { + private T resolveRoot(Class definition) { InternalSchemaSwaps schemaSwaps = new InternalSchemaSwaps(); - return internalFromImpl(definition, new LinkedHashMap<>(), schemaSwaps, ignore); - } - - private static ClassRef extractClassRef(Object type) { - if (type != null) { - if (type instanceof ClassRef) { - return (ClassRef) type; - } else if (type instanceof Class) { - return Types.typeDefFrom((Class) type).toReference(); - } else { - throw new IllegalArgumentException("Unmanaged type passed to the annotation " + type); - } - } else { - return null; + JsonSchema schema = toJsonSchema(definition); + if (schema instanceof GeneratorObjectSchema) { + return resolveObject(new LinkedHashMap<>(), schemaSwaps, schema, "kind", "apiVersion", "metadata"); } + return resolvePropertySchema(new LinkedHashMap<>(), schemaSwaps, null, resolvingContext.serializationConfig.constructType(definition), schema); } - private void extractSchemaSwaps(ClassRef definitionType, AnnotationRef annotation, InternalSchemaSwaps schemaSwaps) { - String fullyQualifiedName = annotation.getClassRef().getFullyQualifiedName(); - switch (fullyQualifiedName) { - case ANNOTATION_SCHEMA_SWAP: - extractSchemaSwap(definitionType, annotation, schemaSwaps); - break; - case ANNOTATION_SCHEMA_SWAPS: - Map params = annotation.getParameters(); - Object[] values = (Object[]) params.get("value"); - for (Object value : values) { - extractSchemaSwap(definitionType, value, schemaSwaps); - } - break; + private static void extractAnnotations(Class beanClass, Class annotation, Consumer consumer) { + while (beanClass != Object.class) { + Stream.of(beanClass.getAnnotationsByType(annotation)).forEach(consumer); + beanClass = beanClass.getSuperclass(); } } - private void extractSchemaSwap(ClassRef definitionType, Object annotation, InternalSchemaSwaps schemaSwaps) { - if (annotation instanceof SchemaSwap) { - SchemaSwap schemaSwap = (SchemaSwap) annotation; - schemaSwaps.registerSwap(definitionType, - extractClassRef(schemaSwap.originalType()), - schemaSwap.fieldName(), - extractClassRef(schemaSwap.targetType()), schemaSwap.depth()); - - } else if (annotation instanceof AnnotationRef - && ((AnnotationRef) annotation).getClassRef().getFullyQualifiedName().equals(ANNOTATION_SCHEMA_SWAP)) { - Map params = ((AnnotationRef) annotation).getParameters(); - schemaSwaps.registerSwap(definitionType, - extractClassRef(params.get("originalType")), - (String) params.get("fieldName"), - extractClassRef(params.getOrDefault("targetType", void.class)), (Integer) params.getOrDefault("depth", 0)); - - } else { - throw new IllegalArgumentException("Unmanaged annotation type passed to the SchemaSwaps: " + annotation); + static void collectValidationRules(BeanProperty beanProperty, List validationRules) { + // TODO: the old logic allowed for picking up the annotation from both the getter and the field + // this requires a messy hack by convention because there doesn't seem to be a way to all annotations + // nor does jackson provide the field + if (beanProperty.getMember() instanceof AnnotatedMethod) { + // field first + Method m = ((AnnotatedMethod)beanProperty.getMember()).getMember(); + String name = m.getName(); + if (name.startsWith("get") || name.startsWith("set")) { + name = name.substring(3); + } else if (name.startsWith("is")) { + name = name.substring(2); + } + if (name.length() > 0) { + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + try { + Field f = beanProperty.getMember().getDeclaringClass().getDeclaredField(name); + ofNullable(f.getAnnotation(ValidationRule.class)).map(KubernetesValidationRule::from) + .ifPresent(validationRules::add); + ofNullable(f.getAnnotation(ValidationRules.class)) + .ifPresent(ann -> Stream.of(ann.value()).map(KubernetesValidationRule::from).forEach(validationRules::add)); + } catch (NoSuchFieldException | SecurityException e) { + } + // then method + Stream.of(m.getAnnotationsByType(ValidationRule.class)).map(KubernetesValidationRule::from).forEach(validationRules::add); + return; } - } - private static Stream extractKubernetesValidationRules(AnnotationRef annotationRef) { - switch (annotationRef.getClassRef().getFullyQualifiedName()) { - case ANNOTATION_VALIDATION_RULE: - return Stream.of(KubernetesValidationRule.from(annotationRef)); - case ANNOTATION_VALIDATION_RULES: - return Arrays.stream(((ValidationRule[]) annotationRef.getParameters().get(VALUE))) - .map(KubernetesValidationRule::from); - default: - return Stream.empty(); - } + // fall back to standard logic + ofNullable(beanProperty.getAnnotation(ValidationRule.class)).map(KubernetesValidationRule::from) + .ifPresent(validationRules::add); + ofNullable(beanProperty.getAnnotation(ValidationRules.class)) + .ifPresent(ann -> Stream.of(ann.value()).map(KubernetesValidationRule::from).forEach(validationRules::add)); } - private T internalFromImpl(TypeDef definition, LinkedHashMap visited, InternalSchemaSwaps schemaSwaps, - String... ignore) { - Set ignores = ignore.length > 0 ? new LinkedHashSet<>(Arrays.asList(ignore)) - : Collections - .emptySet(); - List required = new ArrayList<>(); + class PropertyMetadata { - final boolean isJsonNode = (definition.getFullyQualifiedName() != null && - (definition.getFullyQualifiedName().equals(JSON_NODE_TYPE) - || definition.getFullyQualifiedName().equals(ANY_TYPE))); - - final B builder = (isJsonNode) ? newBuilder(null) : newBuilder(); + private boolean required; + private String description; + private String defaultValue; + private Double min; + private Double max; + private String pattern; + private boolean nullable; + private String format; + private List validationRules = new ArrayList<>(); + private boolean preserveUnknownFields; + private Class schemaFrom; - boolean preserveUnknownFields = isJsonNode; + public PropertyMetadata(JsonSchema value, BeanProperty beanProperty) { + required = Boolean.TRUE.equals(value.getRequired()); - schemaSwaps = schemaSwaps.branchAnnotations(); - final InternalSchemaSwaps swaps = schemaSwaps; - definition.getAnnotations().forEach(annotation -> extractSchemaSwaps(definition.toReference(), annotation, swaps)); + description = beanProperty.getMetadata().getDescription(); + defaultValue = beanProperty.getMetadata().getDefaultValue(); - // index potential accessors by name for faster lookup - final Map accessors = indexPotentialAccessors(definition); + schemaFrom = ofNullable(beanProperty.getAnnotation(SchemaFrom.class)).map(SchemaFrom::type).orElse(null); - for (Property property : definition.getProperties()) { - if (isJsonNode) { - break; + if (value.isValueTypeSchema()) { + ValueTypeSchema valueTypeSchema = value.asValueTypeSchema(); + this.format = ofNullable(valueTypeSchema.getFormat()).map(Object::toString).orElse(null); } - String name = property.getName(); - if (property.isStatic() || ignores.contains(name)) { - LOGGER.debug("Ignoring property {}", name); - continue; + + if (value.isStringSchema()) { + StringSchema stringSchema = value.asStringSchema(); + // only set if ValidationSchemaFactoryWrapper is used + this.pattern = stringSchema.getPattern(); // looks for the Pattern annotation + this.max = ofNullable(stringSchema.getMaxLength()).map(Integer::doubleValue).orElse(null); + this.min = ofNullable(stringSchema.getMinLength()).map(Integer::doubleValue).orElse(null); + } else { + // TODO: process the other schema types for validation values } - schemaSwaps = schemaSwaps.branchDepths(); - SwapResult swapResult = schemaSwaps.lookupAndMark(definition.toReference(), name); - LinkedHashMap savedVisited = visited; - if (swapResult.onGoing) { - visited = new LinkedHashMap<>(); + collectValidationRules(beanProperty, validationRules); + + // TODO: should probably move to a standard annotations + // see ValidationSchemaFactoryWrapper + nullable = beanProperty.getAnnotation(Nullable.class) != null; + max = ofNullable(beanProperty.getAnnotation(Max.class)).map(Max::value).orElse(max); + min = ofNullable(beanProperty.getAnnotation(Min.class)).map(Min::value).orElse(min); + + // TODO: should the following be deprecated? + required = beanProperty.getAnnotation(Required.class) != null; + defaultValue = ofNullable(beanProperty.getAnnotation(Default.class)).map(Default::value).orElse(defaultValue); + pattern = ofNullable(beanProperty.getAnnotation(Pattern.class)).map(Pattern::value).orElse(pattern); + preserveUnknownFields = beanProperty.getAnnotation(PreserveUnknownFields.class) != null; + } + + public T updateSchema(T schema) { + if (description != null) { + schema = addDescription(schema, description); } - final PropertyFacade facade = new PropertyFacade(property, accessors, swapResult.classRef); - final Property possiblyRenamedProperty = facade.process(); - name = possiblyRenamedProperty.getName(); - if (facade.required) { - required.add(name); - } else if (facade.ignored) { - continue; + if (defaultValue != null) { + try { + setDefault(schema, YAML_MAPPER.readTree(defaultValue)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Cannot parse default value: '" + defaultValue + "' as valid YAML."); + } } - final T schema = internalFromImpl(name, possiblyRenamedProperty.getTypeRef(), visited, schemaSwaps); - visited = savedVisited; - if (facade.preserveUnknownFields) { - preserveUnknownFields = true; + if (nullable) { + setNullable(schema, nullable); } - - // if we got a description from the field or an accessor, use it - final String description = facade.description; - final T possiblyUpdatedSchema; - if (description == null) { - possiblyUpdatedSchema = schema; - } else { - possiblyUpdatedSchema = addDescription(schema, description); + setMax(schema, max); + setMin(schema, min); + setPattern(schema, pattern); + setFormat(schema, format); + if (preserveUnknownFields) { + setPreserveUnknown(schema, true); } - SchemaPropsOptions options = new SchemaPropsOptions( - facade.defaultValue, - facade.min, - facade.max, - facade.pattern, - facade.validationRules, - facade.nullable, - facade.required, - facade.preserveUnknownFields); - - addProperty(possiblyRenamedProperty, builder, possiblyUpdatedSchema, options); - } + if (!validationRules.isEmpty()) { + List validationRulesFromProperty = validationRules.stream() + .map(AbstractJsonSchema.this::mapValidationRule) + .collect(Collectors.toList()); - List validationRules = Stream - .concat(definition.getAnnotations().stream(), definition.getExtendsList().stream() - .flatMap(classRef -> GetDefinition.of(classRef).getAnnotations().stream())) - .flatMap(AbstractJsonSchema::extractKubernetesValidationRules) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - swaps.throwIfUnmatchedSwaps(); - return build(builder, required, validationRules, preserveUnknownFields); + schema = addToValidationRules(schema, validationRulesFromProperty); + } + return schema; + } } - private Map indexPotentialAccessors(TypeDef definition) { - final List methods = definition.getMethods(); - final Map accessors = new HashMap<>(methods.size()); - methods.stream() - .filter(this::isPotentialAccessor) - .forEach(m -> accessors.put(m.getName(), m)); - return accessors; - } + private T resolveObject(LinkedHashMap visited, InternalSchemaSwaps schemaSwaps, JsonSchema jacksonSchema, + String... ignore) { + Set ignores = ignore.length > 0 ? new LinkedHashSet<>(Arrays.asList(ignore)) : Collections.emptySet(); - private static class PropertyOrAccessor { - private final Collection annotations; - private final String name; - private final String propertyName; - private final String type; - private String renamedTo; - private String defaultValue; - private Double min; - private Double max; - private String pattern; - private List validationRules; - private boolean nullable; - private boolean required; - private boolean ignored; - private boolean preserveUnknownFields; - private String description; - private TypeRef schemaFrom; + final B builder = newBuilder(); - private PropertyOrAccessor(Collection annotations, String name, String propertyName, boolean isMethod) { - this.annotations = annotations; - this.name = name; - this.propertyName = propertyName; - type = isMethod ? "accessor" : "field"; - } + schemaSwaps = schemaSwaps.branchAnnotations(); + final InternalSchemaSwaps swaps = schemaSwaps; - static PropertyOrAccessor fromProperty(Property property) { - return new PropertyOrAccessor(property.getAnnotations(), property.getName(), property.getName(), false); - } + GeneratorObjectSchema gos = (GeneratorObjectSchema)jacksonSchema.asObjectSchema(); + AnnotationIntrospector ai = resolvingContext.serializationConfig.getAnnotationIntrospector(); + BeanDescription bd = resolvingContext.serializationConfig.introspect(gos.javaType); + boolean preserveUnknownFields = bd.findAnyGetter() != null || bd.findAnySetterAccessor() != null; - static PropertyOrAccessor fromMethod(Method method, String propertyName) { - return new PropertyOrAccessor(method.getAnnotations(), method.getName(), propertyName, true); - } + extractAnnotations(gos.javaType.getRawClass(), SchemaSwap.class, ss -> { + swaps.registerSwap(gos.javaType.getRawClass(), + ss.originalType(), + ss.fieldName(), + ss.targetType(), ss.depth()); + }); - public void process() { - annotations.forEach(a -> { - switch (a.getClassRef().getFullyQualifiedName()) { - case ANNOTATION_DEFAULT: - defaultValue = (String) a.getParameters().get(VALUE); - break; - case ANNOTATION_NULLABLE: - nullable = true; - break; - case ANNOTATION_MAX: - max = (Double) a.getParameters().get(VALUE); - break; - case ANNOTATION_MIN: - min = (Double) a.getParameters().get(VALUE); - break; - case ANNOTATION_PATTERN: - pattern = (String) a.getParameters().get(VALUE); - break; - case ANNOTATION_REQUIRED: - required = true; - break; - case ANNOTATION_JSON_PROPERTY: - final String nameFromAnnotation = (String) a.getParameters().get(VALUE); - if (!Strings.isNullOrEmpty(nameFromAnnotation) && !propertyName.equals(nameFromAnnotation)) { - renamedTo = nameFromAnnotation; - } - break; - case ANNOTATION_JSON_PROPERTY_DESCRIPTION: - final String descriptionFromAnnotation = (String) a.getParameters().get(VALUE); - if (!Strings.isNullOrEmpty(descriptionFromAnnotation)) { - description = descriptionFromAnnotation; - } - break; - case ANNOTATION_JSON_IGNORE: - ignored = true; - break; - case ANNOTATION_JSON_ANY_GETTER: - case ANNOTATION_JSON_ANY_SETTER: - case ANNOTATION_PERSERVE_UNKNOWN_FIELDS: - preserveUnknownFields = true; - break; - case ANNOTATION_SCHEMA_FROM: - schemaFrom = extractClassRef(a.getParameters().get("type")); - break; - case ANNOTATION_VALIDATION_RULE: - case ANNOTATION_VALIDATION_RULES: - validationRules = extractKubernetesValidationRules(a).collect(Collectors.toList()); - break; - } - }); - } + List required = new ArrayList<>(); - public String getRenamedTo() { - return renamedTo; - } + for (Map.Entry property : new TreeMap<>(gos.getProperties()).entrySet()) { + String name = property.getKey(); + if (ignores.contains(name)) { + continue; + } + schemaSwaps = schemaSwaps.branchDepths(); + SwapResult swapResult = schemaSwaps.lookupAndMark(gos.javaType.getRawClass(), name); + LinkedHashMap savedVisited = visited; + if (swapResult.onGoing) { + visited = new LinkedHashMap<>(); + } - public boolean isNullable() { - return nullable; - } + BeanProperty beanProperty = gos.beanProperties.get(property.getKey()); + Utils.checkNotNull(beanProperty, "CRD generation works only with bean properties"); - public Optional getDefault() { - return Optional.ofNullable(defaultValue); - } + JsonSchema propertySchema = property.getValue(); + PropertyMetadata propertyMetadata = new PropertyMetadata(propertySchema, beanProperty); - public Optional getMax() { - return Optional.ofNullable(max); - } + // fallback to the JsonFormat pattern + if (propertyMetadata.pattern == null) { + propertyMetadata.pattern = ofNullable(ai.findFormat(beanProperty.getMember())).map(Value::getPattern).orElse(null); + } - public Optional getMin() { - return Optional.ofNullable(min); - } + if (propertyMetadata.required) { + required.add(name); + } - public Optional getPattern() { - return Optional.ofNullable(pattern); - } + JavaType type = beanProperty.getType(); + if (swapResult.classRef != null) { + propertyMetadata.schemaFrom = swapResult.classRef; + } + if (propertyMetadata.schemaFrom != null) { + if (propertyMetadata.schemaFrom == void.class) { + // fully omit - this is a little inconsistent with the NullSchema handling + continue; + } + propertySchema = toJsonSchema(propertyMetadata.schemaFrom); + type = resolvingContext.serializationConfig.constructType(propertyMetadata.schemaFrom); + } - public Optional> getValidationRules() { - return Optional.ofNullable(validationRules); - } + T schema = resolvePropertySchema(visited, schemaSwaps, name, type, propertySchema); - public boolean isRequired() { - return required; - } + if (!swapResult.onGoing) { + for (Entry, LinkedHashMap> entry : pathMetadata.entrySet()) { + String jsonType = getType(schema); + ofNullable(beanProperty.getAnnotation(entry.getKey())).ifPresent( + ann -> entry.getValue().put(toFQN(savedVisited, name), + new AnnotationMetadata(ann, propertyMetadata.description, jsonType))); + } + } - public boolean isIgnored() { - return ignored; - } + schema = propertyMetadata.updateSchema(schema); - public boolean isPreserveUnknownFields() { - return preserveUnknownFields; - } + visited = savedVisited; - public String getDescription() { - return description; + addProperty(name, builder, schema); } - public boolean contributeName() { - return renamedTo != null; - } + List validationRules = new ArrayList<>(); + extractAnnotations(gos.javaType.getRawClass(), ValidationRule.class, + v -> validationRules.add(KubernetesValidationRule.from(v))); - public boolean contributeDescription() { - return description != null; - } + swaps.throwIfUnmatchedSwaps(); + return build(builder, required, validationRules, preserveUnknownFields); + } - public TypeRef getSchemaFrom() { - return schemaFrom; - } + protected abstract String getType(T schema); - public boolean contributeSchemaFrom() { - return schemaFrom != null; + static String toFQN(LinkedHashMap visited, String name) { + if (visited.isEmpty()) { + return "." + name; } + return visited.values().stream().collect(Collectors.joining(".", ".", ".")) + name; + } + + private T resolvePropertySchema(LinkedHashMap visited, InternalSchemaSwaps schemaSwaps, String name, + JavaType type, JsonSchema jacksonSchema) { - @Override - public String toString() { - return "'" + name + "' " + type; + if (type.getRawClass() == IntOrString.class || type.getRawClass() == Quantity.class) { + return intOrString(); // TODO: create a serializer for this and remove this override } - } - private static class PropertyFacade { - private final List propertyOrAccessors = new ArrayList<>(4); - private String renamedTo; - private String description; - private String defaultValue; - private Double min; - private Double max; - private String pattern; - private boolean nullable; - private boolean required; - private boolean ignored; - private boolean preserveUnknownFields; - private final Property original; - private String nameContributedBy; - private String descriptionContributedBy; - private TypeRef schemaFrom; - private List validationRules; - - public PropertyFacade(Property property, Map potentialAccessors, ClassRef schemaSwap) { - original = property; - final String capitalized = property.getNameCapitalized(); - final String name = property.getName(); - propertyOrAccessors.add(PropertyOrAccessor.fromProperty(property)); - Method method = potentialAccessors.get("is" + capitalized); - if (method != null) { - propertyOrAccessors.add(PropertyOrAccessor.fromMethod(method, name)); + if (jacksonSchema.isArraySchema()) { + Items items = jacksonSchema.asArraySchema().getItems(); + if (items.isArrayItems()) { + throw new IllegalStateException("not yet supported"); } - method = potentialAccessors.get("get" + capitalized); - if (method != null) { - propertyOrAccessors.add(PropertyOrAccessor.fromMethod(method, name)); + JsonSchema arraySchema = jacksonSchema.asArraySchema().getItems().asSingleItems().getSchema(); + final T schema = resolvePropertySchema(visited, schemaSwaps, name, type.getContentType(), arraySchema); + return arrayLikeProperty(schema); + } else if (jacksonSchema.isIntegerSchema()) { + return singleProperty("integer"); + } else if (jacksonSchema.isNumberSchema()) { + return singleProperty("number"); + } else if (jacksonSchema.isBooleanSchema()) { + return singleProperty("boolean"); + } else if (jacksonSchema.isStringSchema()) { + // currently on string enums are supported + StringSchema stringSchema = jacksonSchema.asStringSchema(); + if (!stringSchema.getEnums().isEmpty()) { + Set ignores = type.isEnumType() ? findIngoredEnumConstants(type) : Collections.emptySet(); + final JsonNode[] enumValues = stringSchema.getEnums().stream() + .sorted() + .filter(s -> !ignores.contains(s)) + .map(JsonNodeFactory.instance::textNode) + .toArray(JsonNode[]::new); + return enumProperty(enumValues); } - method = potentialAccessors.get("set" + capitalized); - if (method != null) { - propertyOrAccessors.add(PropertyOrAccessor.fromMethod(method, name)); + return singleProperty("string"); + } else if (jacksonSchema.isNullSchema()) { + return singleProperty("object"); // TODO: this may not be the right choice, but rarely will someone be using Void + } else if (jacksonSchema.isAnySchema()) { + // TODO: this could optionally take a type restriction + T schema = singleProperty(null); + setPreserveUnknown(schema, true); + return schema; + } else if (jacksonSchema.isUnionTypeSchema()) { + throw new IllegalStateException("not yet supported"); + } else if (jacksonSchema instanceof ReferenceSchema) { + // de-reference the reference schema - these can be naturally non-cyclic, for example siblings + ReferenceSchema ref = (ReferenceSchema)jacksonSchema; + GeneratorObjectSchema referenced = resolvingContext.seen.get(ref.get$ref()); + Utils.checkNotNull(referenced, "Could not find previously generated schema"); + jacksonSchema = referenced; + } + + if (type.isMapLikeType()) { + final JavaType keyType = type.getKeyType(); + + if (keyType.getRawClass() != String.class) { + LOGGER.warn("Property '{}' with '{}' key type is mapped to 'string' because of CRD schemas limitations", name, keyType); } - schemaFrom = schemaSwap; - defaultValue = null; - min = null; - max = null; - pattern = null; - validationRules = new LinkedList<>(); - } - public Property process() { - final String name = original.getName(); - - propertyOrAccessors.forEach(p -> { - p.process(); - final String contributorName = p.toString(); - if (p.contributeName()) { - if (renamedTo == null) { - renamedTo = p.getRenamedTo(); - this.nameContributedBy = contributorName; - } else { - LOGGER.debug("Property {} has already been renamed to {} by {}", name, renamedTo, nameContributedBy); - } - } - - if (p.contributeDescription()) { - if (description == null) { - description = p.getDescription(); - descriptionContributedBy = contributorName; - } else { - LOGGER.debug("Description for property {} has already been contributed by: {}", name, descriptionContributedBy); - } - } - defaultValue = p.getDefault().orElse(defaultValue); - min = p.getMin().orElse(min); - max = p.getMax().orElse(max); - pattern = p.getPattern().orElse(pattern); - p.getValidationRules().ifPresent(rules -> validationRules.addAll(rules)); - - if (p.isNullable()) { - nullable = true; - } + final JavaType valueType = type.getContentType(); + JsonSchema mapValueSchema = ((SchemaAdditionalProperties)((ObjectSchema)jacksonSchema).getAdditionalProperties()).getJsonSchema(); + T component = resolvePropertySchema(visited, schemaSwaps, name, valueType, mapValueSchema); + return mapLikeProperty(component); + } - if (p.isRequired()) { - required = true; - } else if (p.isIgnored()) { - ignored = true; - } + Class def = type.getRawClass(); + if (visited.put(def.getName(), name) != null) { + throw new IllegalArgumentException( + "Found a cyclic reference involving the field of type " + def.getName() + " starting a field " + + visited.entrySet().stream().map(e -> e.getValue() + " >>\n" + e.getKey()).collect(Collectors.joining(".")) + "." + + name); + } - preserveUnknownFields = p.isPreserveUnknownFields() || preserveUnknownFields; + T res = resolveObject(visited, schemaSwaps, jacksonSchema); + visited.remove(def.getName()); + return res; + } - if (p.contributeSchemaFrom()) { - schemaFrom = p.getSchemaFrom(); + /** + * we've added support for ignoring an enum values, which complicates this processing + * as that is something not supported directly by jackson + */ + private Set findIngoredEnumConstants(JavaType type) { + Field[] fields = type.getRawClass().getFields(); + Set toIgnore = new HashSet<>(); + for (Field field : fields) { + if (field.isEnumConstant() && field.getAnnotation(JsonIgnore.class) != null) { + // hack to figure out the enum constant + try { + Object value = field.get(null); + toIgnore.add(resolvingContext.kubernetesSerialization.unmarshal(resolvingContext.kubernetesSerialization.asJson(value), String.class)); + } catch (IllegalArgumentException | IllegalAccessException e) { } - }); - - TypeRef typeRef = schemaFrom != null ? schemaFrom : original.getTypeRef(); - String finalName = renamedTo != null ? renamedTo : original.getName(); - - return new Property(original.getAnnotations(), typeRef, finalName, - original.getComments(), false, false, original.getModifiers(), original.getAttributes()); + } } + return toIgnore; } /** @@ -685,17 +513,6 @@ public String getRule() { return rule; } - static KubernetesValidationRule from(AnnotationRef annotationRef) { - KubernetesValidationRule result = new KubernetesValidationRule(); - result.rule = (String) annotationRef.getParameters().get(VALUE); - result.reason = mapNotEmpty((String) annotationRef.getParameters().get("reason")); - result.message = mapNotEmpty((String) annotationRef.getParameters().get("message")); - result.messageExpression = mapNotEmpty((String) annotationRef.getParameters().get("messageExpression")); - result.fieldPath = mapNotEmpty((String) annotationRef.getParameters().get("fieldPath")); - result.optionalOldSelf = Boolean.TRUE.equals(annotationRef.getParameters().get("optionalOldSelf")) ? Boolean.TRUE : null; - return result; - } - static KubernetesValidationRule from(ValidationRule validationRule) { KubernetesValidationRule result = new KubernetesValidationRule(); result.rule = validationRule.value(); @@ -716,65 +533,21 @@ private static String mapNotEmpty(String s) { } } - private boolean isPotentialAccessor(Method method) { - final String name = method.getName(); - return name.startsWith("is") || name.startsWith("get") || name.startsWith("set"); - } - - /** - * Retrieves the updated property name for the specified property if its annotations warrant it - * - * @param property the Property which name might need to be updated - * @return the updated property name or its original one if it didn't need to be changed - */ - private String extractUpdatedNameFromJacksonPropertyIfPresent(Property property) { - final String name = property.getName(); - final boolean ignored = property.getAnnotations().stream() - .anyMatch(a -> a.getClassRef().getFullyQualifiedName().equals(ANNOTATION_JSON_IGNORE)); - - if (ignored) { - return null; - } else { - return property.getAnnotations().stream() - // only consider JsonProperty annotation - .filter(a -> a.getClassRef().getFullyQualifiedName().equals(ANNOTATION_JSON_PROPERTY)) - .findAny() - // if we found an annotated accessor, override the property's name if needed - .map(a -> { - final String fromAnnotation = (String) a.getParameters().get(VALUE); - if (!Strings.isNullOrEmpty(fromAnnotation) && !name.equals(fromAnnotation)) { - return fromAnnotation; - } else { - return name; - } - }).orElse(property.getName()); - } - } - /** * Creates a new specific builder object. * * @return a new builder object specific to the CRD generation version */ - public abstract B newBuilder(); + protected abstract B newBuilder(); /** - * Creates a new specific builder object. + * Adds the specified property to the specified builder * - * @param type the type to be used - * @return a new builder object specific to the CRD generation version - */ - public abstract B newBuilder(String type); - - /** - * Adds the specified property to the specified builder, calling {@link #internalFrom(String, TypeRef)} - * to create the property schema. - * - * @param property the property to add to the currently being built schema + * @param name the property to add to the currently being built schema * @param builder the builder representing the schema being built * @param schema the built schema for the property being added */ - public abstract void addProperty(Property property, B builder, T schema, SchemaPropsOptions options); + protected abstract void addProperty(String name, B builder, T schema); /** * Finishes up the process by actually building the final JSON schema based on the provided @@ -791,149 +564,20 @@ public abstract T build(B builder, List validationRules, boolean preserveUnknownFields); - /** - * Builds the specific JSON schema representing the structural schema for the specified property - * - * @param name the name of the property which schema we want to build - * @param typeRef the type of the property which schema we want to build - * @return the structural schema associated with the specified property - */ - public T internalFrom(String name, TypeRef typeRef) { - return internalFromImpl(name, typeRef, new LinkedHashMap<>(), new InternalSchemaSwaps()); - } - - private T internalFromImpl(String name, TypeRef typeRef, LinkedHashMap visited, - InternalSchemaSwaps schemaSwaps) { - // Note that ordering of the checks here is meaningful: we need to check for complex types last - // in case some "complex" types are handled specifically - if (typeRef.getDimensions() > 0 || io.sundr.model.utils.Collections.isCollection(typeRef)) { // Handle Collections & Arrays - //noinspection unchecked - final TypeRef collectionType = TypeAs.combine(TypeAs.UNWRAP_ARRAY_OF, TypeAs.UNWRAP_COLLECTION_OF) - .apply(typeRef); - final T schema = internalFromImpl(name, collectionType, visited, schemaSwaps); - return arrayLikeProperty(schema); - } else if (io.sundr.model.utils.Collections.IS_MAP.apply(typeRef)) { // Handle Maps - final TypeRef keyType = TypeAs.UNWRAP_MAP_KEY_OF.apply(typeRef); - - if (!(keyType instanceof ClassRef && ((ClassRef) keyType).getFullyQualifiedName().equals("java.lang.String"))) { - LOGGER.warn("Property '{}' with '{}' key type is mapped to 'string' because of CRD schemas limitations", name, typeRef); - } - - final TypeRef valueType = TypeAs.UNWRAP_MAP_VALUE_OF.apply(typeRef); - T schema = internalFromImpl(name, valueType, visited, schemaSwaps); - if (schema == null) { - LOGGER.warn( - "Property '{}' with '{}' value type is mapped to 'object' because its CRD representation cannot be extracted.", - name, typeRef); - schema = internalFromImpl(name, OBJECT_REF, visited, schemaSwaps); - } - - return mapLikeProperty(schema); - } else if (io.sundr.model.utils.Optionals.isOptional(typeRef)) { // Handle Optionals - return internalFromImpl(name, TypeAs.UNWRAP_OPTIONAL_OF.apply(typeRef), visited, schemaSwaps); - } else { - final String typeName = COMMON_MAPPINGS.get(typeRef); - if (typeName != null) { // we have a type that we handle specifically - if (INT_OR_STRING_MARKER.equals(typeName)) { // Handle int or string mapped types - return mappedProperty(typeRef); - } else { - return singleProperty(typeName); // Handle Standard Types - } - } else { - if (typeRef instanceof ClassRef) { // Handle complex types - ClassRef classRef = (ClassRef) typeRef; - TypeDef def = Types.typeDefFrom(classRef); - - // check if we're dealing with an enum - if (def.isEnum()) { - final JsonNode[] enumValues = def.getProperties().stream() - .filter(Property::isEnumConstant) - .map(this::extractUpdatedNameFromJacksonPropertyIfPresent) - .filter(Objects::nonNull) - .sorted() - .map(JsonNodeFactory.instance::textNode) - .toArray(JsonNode[]::new); - return enumProperty(enumValues); - } else if (!classRef.getFullyQualifiedName().equals(VOID.getName())) { - return resolveNestedClass(name, def, visited, schemaSwaps); - } - - } - return null; - } - } - } - - private T resolveNestedClass(String name, TypeDef def, LinkedHashMap visited, - InternalSchemaSwaps schemaSwaps) { - 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 " + fullyQualifiedName + " starting a field " - + visited.entrySet().stream().map(e -> e.getValue() + " >>\n" + e.getKey()).collect(Collectors.joining(".")) + "." - + name); - } - - 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; + private JsonSchema toJsonSchema(Class clazz) { 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); + return resolvingContext.generator.generateSchema(clazz); + } catch (JsonMappingException e) { + throw new RuntimeException(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) + * Builds the schema for specifically for intOrString properties * - * @param ref the type of the specifically handled property * @return the property schema */ - protected abstract T mappedProperty(TypeRef ref); + protected abstract T intOrString(); /** * Builds the schema for array-like properties @@ -962,4 +606,23 @@ private T resolveJavaClass(String fullyQualifiedName) { protected abstract T enumProperty(JsonNode... enumValues); protected abstract T addDescription(T schema, String description); + + protected abstract V mapValidationRule(KubernetesValidationRule validationRule); + + protected abstract void setDefault(T schema, JsonNode node); + + protected abstract void setMin(T schema, Double min); + + protected abstract void setMax(T schema, Double max); + + protected abstract void setFormat(T schema, String format); + + protected abstract void setPattern(T schema, String pattern); + + protected abstract void setNullable(T schema, Boolean nullable); + + protected abstract void setPreserveUnknown(T schema, Boolean preserveUnknown); + + protected abstract T addToValidationRules(T schema, List validationRules); + } diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerationInfo.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerationInfo.java index 9ca26367f53..da7272a193a 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerationInfo.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerationInfo.java @@ -15,8 +15,6 @@ */ package io.fabric8.crd.generator; -import io.fabric8.crd.generator.visitor.ClassDependenciesVisitor; - import java.io.File; import java.net.URI; import java.util.HashMap; @@ -37,8 +35,7 @@ public Map> getCRDDetailsPerNameAndVersion() { void add(String crdName, String version, URI fileURI) { crdNameToVersionToCRDInfoMap.computeIfAbsent(crdName, k -> new HashMap<>()) - .put(version, new CRDInfo(crdName, version, new File(fileURI).getAbsolutePath(), - ClassDependenciesVisitor.getDependentClassesFromCRDName(crdName))); + .put(version, new CRDInfo(crdName, version, new File(fileURI).getAbsolutePath())); } public int numberOfGeneratedCRDs() { diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerator.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerator.java index 227618a06ff..d0f3498adec 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerator.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerator.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; -import io.fabric8.crd.generator.utils.Types; import io.fabric8.crd.generator.v1.CustomResourceHandler; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; @@ -58,6 +57,7 @@ public class CRDGenerator { private boolean parallel; private Map infos; + // TODO: why not rely on the standard fabric8 yaml mapping public static final ObjectMapper YAML_MAPPER = JsonMapper.builder(new YAMLFactory() .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) .enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS) @@ -71,7 +71,6 @@ public class CRDGenerator { public CRDGenerator() { resources = new Resources(); - Types.resetGenerationContext(); // make sure the new generator starts up with a clean slate } public CRDGenerator inOutputDir(File outputDir) { diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDInfo.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDInfo.java index 9260d191b32..f3ba0fa9a83 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDInfo.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDInfo.java @@ -15,19 +15,15 @@ */ package io.fabric8.crd.generator; -import java.util.Set; - public class CRDInfo { private final String crdName; private final String crdSpecVersion; private final String filePath; - private final Set dependentClassNames; - public CRDInfo(String crdName, String crdSpecVersion, String filePath, Set dependentClassNames) { + public CRDInfo(String crdName, String crdSpecVersion, String filePath) { this.crdName = crdName; this.crdSpecVersion = crdSpecVersion; this.filePath = filePath; - this.dependentClassNames = dependentClassNames; } public String getCrdName() { @@ -51,7 +47,4 @@ public String getFilePath() { return filePath; } - public Set getDependentClassNames() { - return dependentClassNames; - } } diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CustomResourceInfo.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CustomResourceInfo.java index ddf7e9469ea..61b74f75fff 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CustomResourceInfo.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CustomResourceInfo.java @@ -17,10 +17,12 @@ import io.fabric8.crd.generator.utils.Types; import io.fabric8.crd.generator.utils.Types.SpecAndStatus; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; +import io.fabric8.kubernetes.client.utils.Utils; import io.fabric8.kubernetes.model.Scope; -import io.sundr.model.TypeDef; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +35,6 @@ public class CustomResourceInfo { private static final Logger LOGGER = LoggerFactory.getLogger(CustomResourceInfo.class); - public static final boolean DESCRIBE_TYPE_DEFS = false; private final String group; private final String version; private final String kind; @@ -45,7 +46,7 @@ public class CustomResourceInfo { private final boolean deprecated; private final String deprecationWarning; private final Scope scope; - private final TypeDef definition; + private final Class definition; private final String crClassName; private final String specClassName; private final String statusClassName; @@ -57,7 +58,7 @@ public class CustomResourceInfo { public CustomResourceInfo(String group, String version, String kind, String singular, String plural, String[] shortNames, boolean storage, boolean served, boolean deprecated, String deprecationWarning, - Scope scope, TypeDef definition, String crClassName, + Scope scope, Class definition, String crClassName, String specClassName, String statusClassName, String[] annotations, String[] labels) { this.group = group; this.version = version; @@ -144,7 +145,7 @@ public Optional statusClassName() { return Optional.ofNullable(statusClassName); } - public TypeDef definition() { + public Class definition() { return definition; } @@ -156,30 +157,42 @@ public String[] labels() { return labels; } - public static CustomResourceInfo fromClass(Class> customResource) { + public static CustomResourceInfo fromClass(Class customResource) { try { - final CustomResource instance = customResource.getDeclaredConstructor().newInstance(); + final HasMetadata instance = customResource.getDeclaredConstructor().newInstance(); final String[] shortNames = CustomResource.getShortNames(customResource); - final TypeDef definition = Types.typeDefFrom(customResource); - if (DESCRIBE_TYPE_DEFS) { - Types.output(definition); - } - - final Scope scope = Types.isNamespaced(definition) ? Scope.NAMESPACED : Scope.CLUSTER; + final Scope scope = Utils.isResourceNamespaced(customResource) ? Scope.NAMESPACED : Scope.CLUSTER; - SpecAndStatus specAndStatus = Types.resolveSpecAndStatusTypes(definition); + SpecAndStatus specAndStatus = Types.resolveSpecAndStatusTypes(customResource); if (specAndStatus.isUnreliable()) { LOGGER.warn( "Cannot reliably determine status types for {} because it isn't parameterized with only spec and status types. Status replicas detection will be deactivated.", customResource.getCanonicalName()); } - return new CustomResourceInfo(instance.getGroup(), instance.getVersion(), instance.getKind(), - instance.getSingular(), instance.getPlural(), shortNames, instance.isStorage(), instance.isServed(), - instance.isDeprecated(), instance.getDeprecationWarning(), - scope, definition, + ResourceDefinitionContext rdc = ResourceDefinitionContext.fromResourceType(customResource); + String singular = HasMetadata.getSingular(customResource); + boolean deprecated = CustomResource.getDeprecated(customResource); + String deprecationWarning = CustomResource.getDeprecationWarning(customResource); + boolean storage = CustomResource.getStorage(customResource); + boolean served = CustomResource.getServed(customResource); + + // instance level methods - TODO: deprecate? + if (instance instanceof CustomResource) { + CustomResource cr = (CustomResource)instance; + singular = cr.getSingular(); + deprecated = cr.isDeprecated(); + deprecationWarning = cr.getDeprecationWarning(); + storage = cr.isStorage(); + served = cr.isServed(); + } + + return new CustomResourceInfo(rdc.getGroup(), rdc.getVersion(), rdc.getKind(), + singular, rdc.getPlural(), shortNames, storage, served, + deprecated, deprecationWarning, + scope, customResource, customResource.getCanonicalName(), specAndStatus.getSpecClassName(), specAndStatus.getStatusClassName(), toStringArray(instance.getMetadata().getAnnotations()), toStringArray(instance.getMetadata().getLabels())); diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java index 204149dbd77..c6afba4186f 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java @@ -15,8 +15,6 @@ */ package io.fabric8.crd.generator; -import io.sundr.model.ClassRef; - import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -53,7 +51,7 @@ public InternalSchemaSwaps branchAnnotations() { return new InternalSchemaSwaps(new HashMap<>(), this.swapDepths, combined); } - public void registerSwap(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType, + public void registerSwap(Class definitionType, Class originalType, String fieldName, Class targetType, int depth) { Value value = new Value(definitionType, originalType, fieldName, targetType, depth); Key key = new Key(originalType, fieldName); @@ -67,16 +65,16 @@ public void registerSwap(ClassRef definitionType, ClassRef originalType, String } static class SwapResult { - final ClassRef classRef; + final Class classRef; final boolean onGoing; - public SwapResult(ClassRef classRef, boolean onGoing) { + public SwapResult(Class classRef, boolean onGoing) { this.classRef = classRef; this.onGoing = onGoing; } } - public SwapResult lookupAndMark(ClassRef originalType, String name) { + public SwapResult lookupAndMark(Class originalType, String name) { Key key = new Key(originalType, name); Value value = swaps.getOrDefault(key, parentSwaps.get(key)); if (value != null) { @@ -104,22 +102,14 @@ public void throwIfUnmatchedSwaps() { } private static final class Key { - private final ClassRef originalType; + private final Class originalType; private final String fieldName; - public Key(ClassRef originalType, String fieldName) { + public Key(Class originalType, String fieldName) { this.originalType = originalType; this.fieldName = fieldName; } - public ClassRef getOriginalType() { - return originalType; - } - - public String getFieldName() { - return fieldName; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -140,21 +130,21 @@ public int hashCode() { @Override public String toString() { return new StringJoiner(", ", Key.class.getSimpleName() + "[", "]") - .add("originalType=" + originalType) + .add("originalType=" + originalType.getName()) .add("fieldName='" + fieldName + "'") .toString(); } } private static class Value { - private final ClassRef originalType; + private final Class originalType; private final String fieldName; - private final ClassRef targetType; + private final Class targetType; private boolean used; - private final ClassRef definitionType; + private final Class definitionType; private final int depth; - public Value(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType, int depth) { + public Value(Class definitionType, Class originalType, String fieldName, Class targetType, int depth) { this.definitionType = definitionType; this.originalType = originalType; this.fieldName = fieldName; @@ -167,26 +157,14 @@ private void markUsed() { this.used = true; } - public ClassRef getOriginalType() { - return originalType; - } - - public String getFieldName() { - return fieldName; - } - - public ClassRef getTargetType() { + public Class getTargetType() { return targetType; } - public boolean isUsed() { - return used; - } - @Override public String toString() { - return "@SchemaSwap(originalType=" + originalType + ", fieldName=\"" + fieldName + "\", targetType=" + targetType - + ") on " + definitionType; + return "@SchemaSwap(originalType=" + originalType.getName() + ", fieldName=\"" + fieldName + "\", targetType=" + targetType.getName() + + ") on " + definitionType.getName(); } } } diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/ResolvingContext.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/ResolvingContext.java new file mode 100644 index 00000000000..81f921424c2 --- /dev/null +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/ResolvingContext.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fabric8.crd.generator; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator; +import com.fasterxml.jackson.module.jsonSchema.factories.JsonSchemaFactory; +import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.factories.VisitorContext; +import com.fasterxml.jackson.module.jsonSchema.factories.WrapperFactory; +import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public class ResolvingContext { + + static final class GeneratorObjectSchema extends ObjectSchema { + + JavaType javaType; + Map beanProperties = new LinkedHashMap<>(); + + @Override + public void putOptionalProperty(BeanProperty property, JsonSchema jsonSchema) { + beanProperties.put(property.getName(), property); + super.putOptionalProperty(property, jsonSchema); + } + + @Override + public JsonSchema putProperty(BeanProperty property, JsonSchema value) { + beanProperties.put(property.getName(), property); + return super.putProperty(property, value); + } + + } + + private final class KubernetesSchemaFactoryWrapper extends SchemaFactoryWrapper { + + private KubernetesSchemaFactoryWrapper(SerializerProvider p, WrapperFactory wrapperFactory) { + super(p, wrapperFactory); + this.schemaProvider = new JsonSchemaFactory() { + + @Override + public ObjectSchema objectSchema() { + return new GeneratorObjectSchema(); + } + + }; + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) { + // TODO: jackson should pass in directly here if there's an anyGetter / setter + // so that we may directly mark preserve unknown + JsonObjectFormatVisitor result = super.expectObjectFormat(convertedType); + ((GeneratorObjectSchema)schema).javaType = convertedType; + seen.putIfAbsent(this.visitorContext.getSeenSchemaUri(convertedType), (GeneratorObjectSchema)schema); + return result; + } + } + + final JsonSchemaGenerator generator; + final SerializationConfig serializationConfig; + final KubernetesSerialization kubernetesSerialization; + final Map seen = new HashMap<>(); + + private static class AccessibleKubernetesSerialization extends KubernetesSerialization { + + @Override + public ObjectMapper getMapper() { + return super.getMapper(); + }; + + } + + private static AccessibleKubernetesSerialization DEFAULT_KUBERNETES_SERIALIZATION; + + public static ResolvingContext defaultResolvingContext() { + if (DEFAULT_KUBERNETES_SERIALIZATION == null) { + DEFAULT_KUBERNETES_SERIALIZATION = new AccessibleKubernetesSerialization(); + } + return new ResolvingContext(DEFAULT_KUBERNETES_SERIALIZATION.getMapper(), DEFAULT_KUBERNETES_SERIALIZATION); + } + + public ResolvingContext(ObjectMapper mapper, KubernetesSerialization kubernetesSerialization) { + serializationConfig = mapper.getSerializationConfig(); + this.kubernetesSerialization = kubernetesSerialization; + generator = new JsonSchemaGenerator(mapper, new WrapperFactory() { + + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider provider) { + return new KubernetesSchemaFactoryWrapper(provider, this); + } + + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider provider, VisitorContext rvc) { + SchemaFactoryWrapper wrapper = getWrapper(provider); + wrapper.setVisitorContext(rvc); + return wrapper; + } + + }); + } + +} diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/utils/Types.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/utils/Types.java index 186e0daf1ca..47806730d97 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/utils/Types.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/utils/Types.java @@ -15,163 +15,18 @@ */ package io.fabric8.crd.generator.utils; -import io.fabric8.kubernetes.api.model.Namespaced; -import io.fabric8.kubernetes.client.CustomResource; -import io.sundr.adapter.api.AdapterContext; -import io.sundr.adapter.api.Adapters; -import io.sundr.model.*; -import io.sundr.model.functions.GetDefinition; -import io.sundr.model.repo.DefinitionRepository; -import io.sundr.model.utils.TypeArguments; -import io.sundr.model.visitors.ApplyTypeParamMappingToMethod; -import io.sundr.model.visitors.ApplyTypeParamMappingToProperty; -import io.sundr.model.visitors.ApplyTypeParamMappingToTypeArguments; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; public class Types { private Types() { throw new IllegalStateException("Utility class"); } - private static final Logger LOGGER = LoggerFactory.getLogger(Types.class); - private static final String NAMESPACED = Namespaced.class.getName(); - public static final String JAVA_LANG_VOID = "java.lang.Void"; - public static final AdapterContext REFLECTION_CONTEXT = AdapterContext.create(DefinitionRepository.getRepository()); - - /** - * Make sure the generation context is reset so that types can be properly introspected if classes have changed since the last - * generation round. - */ - public static void resetGenerationContext() { - DefinitionRepository.getRepository().clear(); - } - - public static TypeDef typeDefFrom(Class clazz) { - return unshallow(Adapters.adaptType(clazz, REFLECTION_CONTEXT)); - } - - public static TypeDef unshallow(TypeDef definition) { - // resolve hierarchy - final List classRefs = new ArrayList<>(Types.projectSuperClasses(definition)); - // resolve properties - final List properties = Types.projectProperties(definition); - // re-create TypeDef with all the needed information - return new TypeDef(definition.getKind(), definition.getPackageName(), - definition.getName(), definition.getComments(), definition.getAnnotations(), classRefs, - definition.getImplementsList(), definition.getParameters(), properties, - definition.getConstructors(), definition.getMethods(), definition.getOuterTypeName(), - definition.getInnerTypes(), definition.getModifiers(), definition.getAttributes()); - } - - public static TypeDef typeDefFrom(ClassRef classRef) { - return unshallow(GetDefinition.of(classRef)); - } - - private static TypeDef projectDefinition(ClassRef ref) { - TypeDef definition = GetDefinition.of(ref); - Map mappings = TypeArguments.getGenericArgumentsMappings(ref, definition); - if (mappings.isEmpty()) { - return definition; - } else { - return new TypeDefBuilder(definition) - .accept(new ApplyTypeParamMappingToTypeArguments(mappings)) // existing type arguments must be handled before methods and properties - .accept(new ApplyTypeParamMappingToProperty(mappings), - new ApplyTypeParamMappingToMethod(mappings)) - .build(); - } - } - - private static Set projectSuperClasses(TypeDef definition) { - List extendsList = definition.getExtendsList(); - return extendsList.stream() - .flatMap(s -> Stream.concat(Stream.of(s), projectDefinition(s).getExtendsList().stream())) - .collect(Collectors.toSet()); - } - - /** - * All non-static properties (including inherited). - * - * @param typeDef The type. - * @return A list with all properties. - */ - private static List projectProperties(TypeDef typeDef) { - final String fqn = typeDef.getFullyQualifiedName(); - return Stream.concat( - typeDef.getProperties().stream().filter(p -> { - // enums have self-referential static properties for each enum case so we cannot ignore them - if (typeDef.isEnum()) { - final TypeRef typeRef = p.getTypeRef(); - if (typeRef instanceof ClassRef && fqn.equals(((ClassRef) typeRef).getFullyQualifiedName())) { - // we're dealing with an enum case so keep it - return true; - } - } - // otherwise exclude all static properties - return !p.isStatic(); - }), - typeDef.getExtendsList().stream() - .filter(e -> !e.getFullyQualifiedName().startsWith("java.")) - .flatMap(e -> projectProperties(projectDefinition(e)) - .stream() - .filter(p -> filterCustomResourceProperties(e).test(p)))) - - .collect(Collectors.toList()); - } - - private static Predicate filterCustomResourceProperties(ClassRef ref) { - return p -> !p.isStatic() && - (!ref.getFullyQualifiedName().equals(CUSTOM_RESOURCE_NAME) || - (p.getName().equals("spec") || p.getName().equals("status"))); - } - - public static void output(TypeDef def) { - final StringBuilder builder = new StringBuilder(300); - Types.describeType(def, "", new HashSet<>(), builder); - LOGGER.debug("\n{}", builder); - } - - public static void describeType(TypeDef def, String indent, Set visited, StringBuilder builder) { - if (visited.isEmpty()) { - builder.append(indent).append(def.getFullyQualifiedName()).append("\n"); - } - - visited.add(def.getFullyQualifiedName()); - for (Property property : def.getProperties()) { - TypeRef typeRef = property.getTypeRef(); - if (typeRef instanceof ClassRef) { - final TypeDef typeDef = typeDefFrom((ClassRef) typeRef); - builder.append(indent).append("\t").append(property).append(" - ClassRef [").append(typeDef.getKind()).append("]\n"); - if (!visited.contains(typeDef.getFullyQualifiedName())) { - describeType(typeDef, indent + "\t", visited, builder); - } - } else { - final String type; - if (typeRef instanceof PrimitiveRef) { - type = "PrimitiveRef"; - } else if (typeRef instanceof TypeParamRef) { - type = "TypeParamRef"; - } else if (typeRef instanceof VoidRef) { - type = "VoidRef"; - } else if (typeRef instanceof WildcardRef) { - type = "WildcardRef"; - } else { - type = "Unknown"; - } - builder.append(indent).append("\t").append(property).append(" - ").append(type).append("\n"); - } - } - visited.remove(def.getFullyQualifiedName()); - } - public static class SpecAndStatus { - private static final SpecAndStatus UNRESOLVED = new SpecAndStatus(null, null); private final String specClassName; private final String statusClassName; private final boolean unreliable; @@ -195,81 +50,26 @@ public boolean isUnreliable() { } } - private static final String CUSTOM_RESOURCE_NAME = CustomResource.class.getCanonicalName(); - - public static SpecAndStatus resolveSpecAndStatusTypes(TypeDef definition) { - Optional optionalCustomResourceRef = definition.getExtendsList().stream() - .filter(s -> s.getFullyQualifiedName().equals(CUSTOM_RESOURCE_NAME)).findFirst(); - if (optionalCustomResourceRef.isPresent()) { - ClassRef customResourceRef = optionalCustomResourceRef.get(); - List arguments = customResourceRef.getArguments(); - if (arguments.size() == 2) { - String specClassName = getClassFQNIfNotVoid(arguments.get(0)); - String statusClassName = getClassFQNIfNotVoid(arguments.get(1)); - return new SpecAndStatus(specClassName, statusClassName); - } - } - return SpecAndStatus.UNRESOLVED; - } - - public static boolean isNamespaced(TypeDef definition) { - return isNamespaced(definition, new HashSet<>()); - } - - public static boolean isNamespaced(TypeDef definition, Set visited) { - if (definition.getFullyQualifiedName().equals(NAMESPACED)) { - return true; - } - - if (visited.contains(definition) || definition.getPackageName().startsWith("java.")) { - return false; - } - - Set newVisited = new HashSet<>(visited); - newVisited.add(definition); - - for (ClassRef i : definition.getImplementsList()) { - if (isNamespaced(GetDefinition.of(i), newVisited)) { - return true; - } - } - - for (ClassRef e : definition.getExtendsList()) { - if (isNamespaced(GetDefinition.of(e), newVisited)) { - return true; - } - } - return false; - } - /** - * Finds the status property. The method look up for a status property. + * Determine the spec and status types via convention by looking for the + * spec and status properties. * - * @param typeDef the type whose status property we want to find - * @return the an optional property. - */ - public static Optional findStatusProperty(TypeDef typeDef) { - return typeDef.getProperties().stream() - .filter(Types::isStatusProperty) - .findFirst(); - } - - /** - * Returns true if the specified property corresponds to status. - * A property qualifies as `status` if it is called `status`. - * - * @param property the property we want to check - * @return {@code true} if named {@code status}, {@code false} otherwise + * If we support eventually support spec and status interfaces or some other mechanism + * then this logic will need to change */ - public static boolean isStatusProperty(Property property) { - return "status".equals(property.getName()) && getClassFQNIfNotVoid(property.getTypeRef()) != null; - } - - private static String getClassFQNIfNotVoid(TypeRef typeRef) { - if (!(typeRef instanceof ClassRef)) { - return null; + public static SpecAndStatus resolveSpecAndStatusTypes(Class definition) { + SerializationConfig config = new ObjectMapper().getSerializationConfig(); + BeanDescription description = config.introspect(config.constructType(definition)); + String specClassName = null; + String statusClassName = null; + for (BeanPropertyDefinition bpd : description.findProperties()) { + if (bpd.getName().equals("spec") && bpd.getRawPrimaryType() != Void.class) { + specClassName = bpd.getRawPrimaryType().getName(); + } else if (bpd.getName().equals("status") && bpd.getRawPrimaryType() != Void.class) { + statusClassName = bpd.getRawPrimaryType().getName(); + } } - String fullyQualifiedName = ((ClassRef) typeRef).getFullyQualifiedName(); - return JAVA_LANG_VOID.equals(fullyQualifiedName) ? null : fullyQualifiedName; + return new SpecAndStatus(specClassName, statusClassName); } + } diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/CustomResourceHandler.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/CustomResourceHandler.java index e5b00357b17..f4d30cba556 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/CustomResourceHandler.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/CustomResourceHandler.java @@ -17,7 +17,9 @@ import io.fabric8.crd.generator.AbstractCustomResourceHandler; import io.fabric8.crd.generator.CustomResourceInfo; +import io.fabric8.crd.generator.ResolvingContext; import io.fabric8.crd.generator.Resources; +import io.fabric8.crd.generator.annotation.PrinterColumn; import io.fabric8.crd.generator.decorator.Decorator; import io.fabric8.crd.generator.v1.decorator.AddAdditionPrinterColumnDecorator; import io.fabric8.crd.generator.v1.decorator.AddCustomResourceDefinitionResourceDecorator; @@ -34,16 +36,17 @@ import io.fabric8.crd.generator.v1.decorator.SetStorageVersionDecorator; import io.fabric8.crd.generator.v1.decorator.SortCustomResourceDefinitionVersionDecorator; import io.fabric8.crd.generator.v1.decorator.SortPrinterColumnsDecorator; -import io.sundr.model.TypeDef; - -import java.util.Optional; +import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; +import io.fabric8.kubernetes.model.annotation.LabelSelector; +import io.fabric8.kubernetes.model.annotation.SpecReplicas; +import io.fabric8.kubernetes.model.annotation.StatusReplicas; public class CustomResourceHandler extends AbstractCustomResourceHandler { public static final String VERSION = "v1"; public CustomResourceHandler(Resources resources, boolean parallel) { - super(resources, parallel); + super(resources); } @Override @@ -55,8 +58,7 @@ protected Decorator getPrinterColumnDecorator(String name, } @Override - protected void addDecorators(CustomResourceInfo config, TypeDef def, Optional specReplicasPath, - Optional statusReplicasPath, Optional labelSelectorPath) { + public void handle(CustomResourceInfo config) { final String name = config.crdName(); final String version = config.version(); resources.decorate( @@ -66,24 +68,29 @@ protected void addDecorators(CustomResourceInfo config, TypeDef def, Optional { + resolver.getSinglePath(SpecReplicas.class).ifPresent(path -> { resources.decorate(new AddSubresourcesDecorator(name, version)); resources.decorate(new AddSpecReplicasPathDecorator(name, version, path)); }); - statusReplicasPath.ifPresent(path -> { + resolver.getSinglePath(StatusReplicas.class).ifPresent(path -> { resources.decorate(new AddSubresourcesDecorator(name, version)); resources.decorate(new AddStatusReplicasPathDecorator(name, version, path)); }); - labelSelectorPath.ifPresent(path -> { + resolver.getSinglePath(LabelSelector.class).ifPresent(path -> { resources.decorate(new AddSubresourcesDecorator(name, version)); resources.decorate(new AddLabelSelectorPathDecorator(name, version, path)); }); + handlePrinterColumns(name, version, resolver.getAllPaths(PrinterColumn.class)); + if (config.statusClassName().isPresent()) { resources.decorate(new AddSubresourcesDecorator(name, version)); resources.decorate(new AddStatusSubresourceDecorator(name, version)); @@ -97,8 +104,4 @@ protected void addDecorators(CustomResourceInfo config, TypeDef def, Optional { - - private static final JsonSchema instance = new JsonSchema(); +public class JsonSchema extends AbstractJsonSchema { private static final JSONSchemaProps JSON_SCHEMA_INT_OR_STRING = new JSONSchemaPropsBuilder() .withXKubernetesIntOrString(true) @@ -43,65 +34,63 @@ public class JsonSchema extends AbstractJsonSchema definition) { + return new JsonSchema(ResolvingContext.defaultResolvingContext(), definition).getSchema(); + } + + public JsonSchema(ResolvingContext resolvingContext, Class definition) { + super(resolvingContext, definition); } @Override public JSONSchemaPropsBuilder newBuilder() { - return newBuilder("object"); + return new JSONSchemaPropsBuilder().withType("object"); } @Override - public JSONSchemaPropsBuilder newBuilder(String type) { - final JSONSchemaPropsBuilder builder = new JSONSchemaPropsBuilder(); - builder.withType(type); - return builder; + protected void setDefault(JSONSchemaProps schema, JsonNode node) { + schema.setDefault(node); } @Override - public void addProperty(Property property, JSONSchemaPropsBuilder builder, - JSONSchemaProps schema, SchemaPropsOptions options) { - if (schema != null) { - options.getDefault().ifPresent(s -> { - try { - schema.setDefault(YAML_MAPPER.readTree(s)); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Cannot parse default value: '" + s + "' as valid YAML."); - } - }); - options.getMin().ifPresent(schema::setMinimum); - options.getMax().ifPresent(schema::setMaximum); - options.getPattern().ifPresent(schema::setPattern); + protected void setMin(JSONSchemaProps schema, Double min) { + schema.setMinimum(min); + } - List validationRulesFromProperty = options.getValidationRules().stream() - .map(this::mapValidationRule) - .collect(Collectors.toList()); + @Override + protected void setMax(JSONSchemaProps schema, Double max) { + schema.setMaximum(max); + } - List resultingValidationRules = new ArrayList<>(schema.getXKubernetesValidations()); - resultingValidationRules.addAll(validationRulesFromProperty); + @Override + protected void setFormat(JSONSchemaProps schema, String format) { + schema.setFormat(format); + } - if (!resultingValidationRules.isEmpty()) { - schema.setXKubernetesValidations(resultingValidationRules); - } + @Override + protected void setPattern(JSONSchemaProps schema, String pattern) { + schema.setPattern(pattern); + } - if (options.isNullable()) { - schema.setNullable(true); - } + @Override + protected void setNullable(JSONSchemaProps schema, Boolean nullable) { + schema.setNullable(nullable); + } - if (options.isPreserveUnknownFields()) { - schema.setXKubernetesPreserveUnknownFields(true); - } + @Override + protected void setPreserveUnknown(JSONSchemaProps schema, Boolean preserveUnknown) { + schema.setXKubernetesPreserveUnknownFields(preserveUnknown); + } - builder.addToProperties(property.getName(), schema); - } + @Override + protected JSONSchemaProps addToValidationRules(JSONSchemaProps schema, List validationRules) { + return schema.edit().addAllToXKubernetesValidations(validationRules).build(); + } + + @Override + protected void addProperty(String name, JSONSchemaPropsBuilder builder, + JSONSchemaProps schema) { + builder.addToProperties(name, schema); } @Override @@ -141,7 +130,7 @@ protected JSONSchemaProps singleProperty(String typeName) { } @Override - protected JSONSchemaProps mappedProperty(TypeRef ref) { + protected JSONSchemaProps intOrString() { return JSON_SCHEMA_INT_OR_STRING; } @@ -157,13 +146,8 @@ protected JSONSchemaProps addDescription(JSONSchemaProps schema, String descript .build(); } - private List mapValidationRules(List validationRules) { - return validationRules.stream() - .map(this::mapValidationRule) - .collect(Collectors.toList()); - } - - private ValidationRule mapValidationRule(KubernetesValidationRule validationRule) { + @Override + protected ValidationRule mapValidationRule(KubernetesValidationRule validationRule) { return new ValidationRuleBuilder() .withRule(validationRule.getRule()) .withMessage(validationRule.getMessage()) @@ -173,4 +157,10 @@ private ValidationRule mapValidationRule(KubernetesValidationRule validationRule .withOptionalOldSelf(validationRule.getOptionalOldSelf()) .build(); } + + @Override + protected String getType(JSONSchemaProps schema) { + return schema.getType(); + } + } diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/CustomResourceHandler.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/CustomResourceHandler.java index 5041b845322..beab7ad4d3e 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/CustomResourceHandler.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/CustomResourceHandler.java @@ -17,7 +17,9 @@ import io.fabric8.crd.generator.AbstractCustomResourceHandler; import io.fabric8.crd.generator.CustomResourceInfo; +import io.fabric8.crd.generator.ResolvingContext; import io.fabric8.crd.generator.Resources; +import io.fabric8.crd.generator.annotation.PrinterColumn; import io.fabric8.crd.generator.decorator.Decorator; import io.fabric8.crd.generator.v1beta1.decorator.AddAdditionPrinterColumnDecorator; import io.fabric8.crd.generator.v1beta1.decorator.AddCustomResourceDefinitionResourceDecorator; @@ -35,15 +37,16 @@ import io.fabric8.crd.generator.v1beta1.decorator.SetStorageVersionDecorator; import io.fabric8.crd.generator.v1beta1.decorator.SortCustomResourceDefinitionVersionDecorator; import io.fabric8.crd.generator.v1beta1.decorator.SortPrinterColumnsDecorator; -import io.sundr.model.TypeDef; - -import java.util.Optional; +import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.JSONSchemaProps; +import io.fabric8.kubernetes.model.annotation.LabelSelector; +import io.fabric8.kubernetes.model.annotation.SpecReplicas; +import io.fabric8.kubernetes.model.annotation.StatusReplicas; public class CustomResourceHandler extends AbstractCustomResourceHandler { public static final String VERSION = "v1beta1"; public CustomResourceHandler(Resources resources, boolean parallel) { - super(resources, parallel); + super(resources); } @Override @@ -55,9 +58,7 @@ protected Decorator getPrinterColumnDecorator( } @Override - protected void addDecorators(CustomResourceInfo config, TypeDef def, - Optional specReplicasPath, Optional statusReplicasPath, - Optional labelSelectorPath) { + public void handle(CustomResourceInfo config) { final String name = config.crdName(); final String version = config.version(); resources.decorate( @@ -67,24 +68,29 @@ protected void addDecorators(CustomResourceInfo config, TypeDef def, resources.decorate(new AddCustomResourceDefinitionVersionDecorator(name, version)); + JsonSchema resolver = new JsonSchema(ResolvingContext.defaultResolvingContext(), config.definition()); + JSONSchemaProps schema = resolver.getSchema(); + resources.decorate(new AddSchemaToCustomResourceDefinitionVersionDecorator(name, version, - JsonSchema.from(def, "kind", "apiVersion", "metadata"))); + schema)); - specReplicasPath.ifPresent(path -> { + resolver.getSinglePath(SpecReplicas.class).ifPresent(path -> { resources.decorate(new AddSubresourcesDecorator(name, version)); resources.decorate(new AddSpecReplicasPathDecorator(name, version, path)); }); - statusReplicasPath.ifPresent(path -> { + resolver.getSinglePath(StatusReplicas.class).ifPresent(path -> { resources.decorate(new AddSubresourcesDecorator(name, version)); resources.decorate(new AddStatusReplicasPathDecorator(name, version, path)); }); - labelSelectorPath.ifPresent(path -> { + resolver.getSinglePath(LabelSelector.class).ifPresent(path -> { resources.decorate(new AddSubresourcesDecorator(name, version)); resources.decorate(new AddLabelSelectorPathDecorator(name, version, path)); }); + handlePrinterColumns(name, version, resolver.getAllPaths(PrinterColumn.class)); + if (config.statusClassName().isPresent()) { resources.decorate(new AddSubresourcesDecorator(name, version)); resources.decorate(new AddStatusSubresourceDecorator(name, version)); @@ -99,8 +105,4 @@ protected void addDecorators(CustomResourceInfo config, TypeDef def, resources.decorate(new SortPrinterColumnsDecorator(name, version)); } - @Override - public void handle(CustomResourceInfo config) { - super.handle(config); - } } diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java index 2cf5e6c048e..975f1dd064d 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java @@ -15,94 +15,78 @@ */ package io.fabric8.crd.generator.v1beta1; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import io.fabric8.crd.generator.AbstractJsonSchema; +import io.fabric8.crd.generator.ResolvingContext; import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.JSONSchemaProps; import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.JSONSchemaPropsBuilder; import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.ValidationRule; import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.ValidationRuleBuilder; -import io.sundr.model.Property; -import io.sundr.model.TypeDef; -import io.sundr.model.TypeRef; -import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; -import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER; +public class JsonSchema extends AbstractJsonSchema { -public class JsonSchema extends AbstractJsonSchema { - - private static final JsonSchema instance = new JsonSchema(); - - public static final JSONSchemaProps JSON_SCHEMA_INT_OR_STRING = new JSONSchemaPropsBuilder() + private static final JSONSchemaProps JSON_SCHEMA_INT_OR_STRING = new JSONSchemaPropsBuilder() .withXKubernetesIntOrString(true) .withAnyOf( new JSONSchemaPropsBuilder().withType("integer").build(), new JSONSchemaPropsBuilder().withType("string").build()) .build(); - /** - * Creates the JSON schema for the particular {@link TypeDef}. - * - * @param definition The definition. - * @param ignore an optional list of property names to ignore - * @return The schema. - */ - public static JSONSchemaProps from( - TypeDef definition, String... ignore) { - return instance.internalFrom(definition, ignore); + public JsonSchema(ResolvingContext resolvingContext, Class definition) { + super(resolvingContext, definition); } @Override public JSONSchemaPropsBuilder newBuilder() { - return newBuilder("object"); + return new JSONSchemaPropsBuilder().withType("object"); } @Override - public JSONSchemaPropsBuilder newBuilder(String type) { - final JSONSchemaPropsBuilder builder = new JSONSchemaPropsBuilder(); - builder.withType(type); - return builder; + protected void setDefault(JSONSchemaProps schema, JsonNode node) { + schema.setDefault(node); } @Override - public void addProperty(Property property, JSONSchemaPropsBuilder builder, - JSONSchemaProps schema, SchemaPropsOptions options) { - if (schema != null) { - options.getDefault().ifPresent(s -> { - try { - schema.setDefault(YAML_MAPPER.readTree(s)); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Cannot parse default value: '" + s + "' as valid YAML."); - } - }); - options.getMin().ifPresent(schema::setMinimum); - options.getMax().ifPresent(schema::setMaximum); - options.getPattern().ifPresent(schema::setPattern); + protected void setMin(JSONSchemaProps schema, Double min) { + schema.setMinimum(min); + } - List validationRulesFromProperty = options.getValidationRules().stream() - .map(this::mapValidationRule) - .collect(Collectors.toList()); + @Override + protected void setMax(JSONSchemaProps schema, Double max) { + schema.setMaximum(max); + } - List resultingValidationRules = new ArrayList<>(schema.getXKubernetesValidations()); - resultingValidationRules.addAll(validationRulesFromProperty); + @Override + protected void setFormat(JSONSchemaProps schema, String format) { + schema.setFormat(format); + } - if (!resultingValidationRules.isEmpty()) { - schema.setXKubernetesValidations(resultingValidationRules); - } + @Override + protected void setPattern(JSONSchemaProps schema, String pattern) { + schema.setPattern(pattern); + } + + @Override + protected void setNullable(JSONSchemaProps schema, Boolean nullable) { + schema.setNullable(nullable); + } - if (options.isNullable()) { - schema.setNullable(true); - } + @Override + protected void setPreserveUnknown(JSONSchemaProps schema, Boolean preserveUnknown) { + schema.setXKubernetesPreserveUnknownFields(preserveUnknown); + } - if (options.isPreserveUnknownFields()) { - schema.setXKubernetesPreserveUnknownFields(true); - } + @Override + protected JSONSchemaProps addToValidationRules(JSONSchemaProps schema, List validationRules) { + return schema.edit().addAllToXKubernetesValidations(validationRules).build(); + } - builder.addToProperties(property.getName(), schema); - } + @Override + protected void addProperty(String name, JSONSchemaPropsBuilder builder, + JSONSchemaProps schema) { + builder.addToProperties(name, schema); } @Override @@ -138,13 +122,11 @@ protected JSONSchemaProps mapLikeProperty(JSONSchemaProps schema) { @Override protected JSONSchemaProps singleProperty(String typeName) { - return new JSONSchemaPropsBuilder() - .withType(typeName) - .build(); + return new JSONSchemaPropsBuilder().withType(typeName).build(); } @Override - protected JSONSchemaProps mappedProperty(TypeRef ref) { + protected JSONSchemaProps intOrString() { return JSON_SCHEMA_INT_OR_STRING; } @@ -160,13 +142,8 @@ protected JSONSchemaProps addDescription(JSONSchemaProps schema, String descript .build(); } - private List mapValidationRules(List validationRules) { - return validationRules.stream() - .map(this::mapValidationRule) - .collect(Collectors.toList()); - } - - private ValidationRule mapValidationRule(KubernetesValidationRule validationRule) { + @Override + protected ValidationRule mapValidationRule(KubernetesValidationRule validationRule) { return new ValidationRuleBuilder() .withRule(validationRule.getRule()) .withMessage(validationRule.getMessage()) @@ -176,4 +153,9 @@ private ValidationRule mapValidationRule(KubernetesValidationRule validationRule .withOptionalOldSelf(validationRule.getOptionalOldSelf()) .build(); } + + @Override + protected String getType(JSONSchemaProps schema) { + return schema.getType(); + } } diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AdditionalPrinterColumnDetector.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AdditionalPrinterColumnDetector.java deleted file mode 100644 index 615ee10f7d9..00000000000 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AdditionalPrinterColumnDetector.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.generator.visitor; - -import io.fabric8.crd.generator.annotation.PrinterColumn; - -public class AdditionalPrinterColumnDetector extends AnnotatedMultiPropertyPathDetector { - - public AdditionalPrinterColumnDetector() { - this(DOT); - } - - public AdditionalPrinterColumnDetector(String prefix) { - super(prefix, PrinterColumn.class.getSimpleName()); - } -} diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AnnotatedMultiPropertyPathDetector.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AnnotatedMultiPropertyPathDetector.java deleted file mode 100644 index ca337976bea..00000000000 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AnnotatedMultiPropertyPathDetector.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.generator.visitor; - -import io.fabric8.crd.generator.utils.Types; -import io.sundr.builder.TypedVisitor; -import io.sundr.model.ClassRef; -import io.sundr.model.Property; -import io.sundr.model.TypeDef; -import io.sundr.model.TypeDefBuilder; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import static io.fabric8.crd.generator.AbstractJsonSchema.ANNOTATION_JSON_IGNORE; - -public class AnnotatedMultiPropertyPathDetector extends TypedVisitor { - - protected static final String DOT = "."; - protected static final String STATUS = ".status."; - - private final String prefix; - private final String annotationName; - private final List parents; - private final Map properties; - private final Deque toRun; - - public AnnotatedMultiPropertyPathDetector(String prefix, String annotationName) { - this(prefix, annotationName, new ArrayList<>(), new HashMap<>(), new ArrayDeque<>()); - } - - public AnnotatedMultiPropertyPathDetector(String prefix, String annotationName, List parents, - Map properties, Deque toRun) { - this.prefix = prefix; - this.annotationName = annotationName; - this.parents = parents; - this.properties = properties; - this.toRun = toRun; - } - - private boolean excludePropertyProcessing(Property p) { - return p.getAnnotations().stream() - .anyMatch(ann -> ann.getClassRef().getFullyQualifiedName().equals(ANNOTATION_JSON_IGNORE)); - } - - @Override - public void visit(TypeDefBuilder builder) { - TypeDef type = builder.build(); - final List props = type.getProperties(); - for (Property p : props) { - if (parents.contains(p)) { - continue; - } - - List newParents = new ArrayList<>(parents); - boolean match = p.getAnnotations().stream().anyMatch(a -> a.getClassRef().getName().equals(annotationName)); - if (match) { - newParents.add(p); - this.properties - .put(newParents.stream().map(Property::getName).collect(Collectors.joining(DOT, prefix, "")), p); - } - } - - props.stream().filter(p -> p.getTypeRef() instanceof ClassRef).forEach(p -> { - if (!parents.contains(p) && !excludePropertyProcessing(p)) { - ClassRef classRef = (ClassRef) p.getTypeRef(); - TypeDef propertyType = Types.typeDefFrom(classRef); - if (!propertyType.isEnum() && !classRef.getPackageName().startsWith("java.")) { - List newParents = new ArrayList<>(parents); - newParents.add(p); - toRun.add(() -> new TypeDefBuilder(propertyType) - .accept(new AnnotatedMultiPropertyPathDetector(prefix, annotationName, newParents, properties, toRun))); - } - } - }); - - if (parents.isEmpty()) { - while (!toRun.isEmpty()) { - toRun.pop().run(); - } - } - } - - public Set getPaths() { - return properties.keySet(); - } - - public Map getProperties() { - return properties; - } -} diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AnnotatedPropertyPathDetector.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AnnotatedPropertyPathDetector.java deleted file mode 100644 index 107d4bdb82a..00000000000 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/AnnotatedPropertyPathDetector.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.generator.visitor; - -import io.fabric8.crd.generator.utils.Types; -import io.sundr.builder.TypedVisitor; -import io.sundr.model.AnnotationRef; -import io.sundr.model.ClassRef; -import io.sundr.model.Property; -import io.sundr.model.TypeDef; -import io.sundr.model.TypeDefBuilder; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import static io.fabric8.crd.generator.AbstractJsonSchema.ANNOTATION_JSON_IGNORE; - -public class AnnotatedPropertyPathDetector extends TypedVisitor { - - protected static final String DOT = "."; - protected static final String STATUS = ".status."; - - private final String prefix; - private final String annotationName; - private final List parents; - private final AtomicReference reference; - private final Deque toRun; - - public AnnotatedPropertyPathDetector(String prefix, String annotationName) { - this(prefix, annotationName, new ArrayList<>(), new AtomicReference<>(), new ArrayDeque<>()); - } - - public AnnotatedPropertyPathDetector(String prefix, String annotationName, List parents, - AtomicReference reference, Deque toRun) { - this.prefix = prefix; - this.annotationName = annotationName; - this.parents = parents; - this.reference = reference; - this.toRun = toRun; - } - - private static boolean excludePropertyProcessing(Property p) { - for (AnnotationRef annotation : p.getAnnotations()) { - if (annotation.getClassRef().getFullyQualifiedName().equals(ANNOTATION_JSON_IGNORE)) { - return true; - } - } - return false; - } - - @Override - public void visit(TypeDefBuilder builder) { - TypeDef type = builder.build(); - final List properties = type.getProperties(); - visitProperties(properties); - } - - private void visitProperties(List properties) { - for (Property p : properties) { - if (parents.contains(p)) { - continue; - } - - List newParents = new ArrayList<>(parents); - boolean match = false; - for (AnnotationRef annotation : p.getAnnotations()) { - match = annotation.getClassRef().getName().equals(annotationName); - if (match) { - newParents.add(p); - reference.set(newParents.stream().map(Property::getName).collect(Collectors.joining(DOT, prefix, ""))); - return; - } - } - - if (p.getTypeRef() instanceof ClassRef && !excludePropertyProcessing(p)) { - ClassRef classRef = (ClassRef) p.getTypeRef(); - TypeDef propertyType = Types.typeDefFrom(classRef); - if (!propertyType.isEnum() && !classRef.getPackageName().startsWith("java.")) { - newParents.add(p); - new TypeDefBuilder(propertyType) - .accept(new AnnotatedPropertyPathDetector(prefix, annotationName, newParents, reference, toRun)); - } - } - } - - if (parents.isEmpty()) { - while (!toRun.isEmpty() && reference.get() == null) { - toRun.pop().run(); - } - } - } - - public Optional getPath() { - return Optional.ofNullable(reference.get()); - } -} diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/ClassDependenciesVisitor.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/ClassDependenciesVisitor.java deleted file mode 100644 index e45bf18e7fb..00000000000 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/ClassDependenciesVisitor.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.generator.visitor; - -import io.fabric8.crd.generator.utils.Types; -import io.sundr.builder.TypedVisitor; -import io.sundr.model.ClassRef; -import io.sundr.model.TypeDef; -import io.sundr.model.TypeDefBuilder; -import io.sundr.model.TypeRef; -import io.sundr.model.utils.Collections; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -public class ClassDependenciesVisitor extends TypedVisitor { - private static final Map> traversedClasses = new HashMap<>(); - private static final Map> crdNameToCrClass = new HashMap<>(); - private final Set classesForCR; - private final Set processed = new HashSet<>(); - - public ClassDependenciesVisitor(String crClassName, String crdName) { - // need to record all classes associated with the different versions of the CR (not the CRD spec) - crdNameToCrClass.computeIfAbsent(crdName, k -> new HashSet<>()).add(crClassName); - classesForCR = traversedClasses.computeIfAbsent(crClassName, k -> new HashSet<>()); - } - - @Override - public void visit(TypeDefBuilder builder) { - TypeDef type = builder.build(); - - // finish quickly if we're ignoring the class or we already have processed it - // note that we cannot simply check the traversed class set to know if a class has been processed because classes - // are usually added to the traversed set before they're looked at in depth - final String className = type.getFullyQualifiedName(); - if (ignore(className)) { - return; - } - - // process all references to see if they need to be added or not - type.getReferences().forEach(c -> { - final String fqn = c.getFullyQualifiedName(); - if (ignore(fqn)) { - return; - } - - // check if we're dealing with a collection type to extract parameter types if existing - if (fqn.startsWith("java.util") && (Collections.isCollection(c) || Collections.IS_MAP.apply(c))) { - c.getArguments().forEach(this::processTypeRef); - } else { - // otherwise, process all non-JDK classes that we haven't already processed - if (!ignore(fqn) && classesForCR.add(fqn)) { - // deal with generic arguments if present - c.getArguments().forEach(this::processTypeRef); - } - } - }); - - // add classes from extends list - type.getExtendsList().forEach(this::processTypeRef); - } - - private boolean ignore(String className) { - return (className.startsWith("java.") && !className.startsWith("java.util.")) - || className.startsWith("com.fasterxml.jackson") || className.startsWith("jdk."); - } - - private void processTypeRef(TypeRef t) { - if (t instanceof ClassRef) { - ClassRef classRef = (ClassRef) t; - // only process the class reference if we haven't already - // note that the references are stored in the set including type arguments, so List and List are not the same - if (processed.add(classRef)) { - visit(new TypeDefBuilder(Types.typeDefFrom(classRef))); - } - } - } - - public static Map> getTraversedClasses() { - return traversedClasses; - } - - public static Set getDependentClasses(String crClassName) { - return traversedClasses.get(crClassName); - } - - public static Set getDependentClassesFromCRDName(String crdName) { - // retrieve all dependent classes that might affect any of the CR versions - return crdNameToCrClass.get(crdName).stream() - .flatMap(crClassName -> traversedClasses.get(crClassName).stream()) - .collect(Collectors.toSet()); - } -} diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/LabelSelectorPathDetector.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/LabelSelectorPathDetector.java deleted file mode 100644 index 808698d03b7..00000000000 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/LabelSelectorPathDetector.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.generator.visitor; - -import io.fabric8.kubernetes.model.annotation.LabelSelector; - -public class LabelSelectorPathDetector extends AnnotatedPropertyPathDetector { - - public LabelSelectorPathDetector() { - this(DOT); - } - - public LabelSelectorPathDetector(String prefix) { - super(prefix, LabelSelector.class.getSimpleName()); - } -} diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/SpecReplicasPathDetector.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/SpecReplicasPathDetector.java deleted file mode 100644 index 8a47193f382..00000000000 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/SpecReplicasPathDetector.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.generator.visitor; - -import io.fabric8.kubernetes.model.annotation.SpecReplicas; - -public class SpecReplicasPathDetector extends AnnotatedPropertyPathDetector { - - public SpecReplicasPathDetector() { - this(DOT); - } - - public SpecReplicasPathDetector(String prefix) { - super(prefix, SpecReplicas.class.getSimpleName()); - } -} diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/StatusReplicasPathDetector.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/StatusReplicasPathDetector.java deleted file mode 100644 index 38581fa90e3..00000000000 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/visitor/StatusReplicasPathDetector.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.generator.visitor; - -import io.fabric8.kubernetes.model.annotation.StatusReplicas; - -public class StatusReplicasPathDetector extends AnnotatedPropertyPathDetector { - - public StatusReplicasPathDetector(String prefix) { - super(prefix, StatusReplicas.class.getSimpleName()); - - } - - public StatusReplicasPathDetector() { - this(DOT); - } -} diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/alt/LabelSelector.java b/crd-generator/api/src/test/java/io/fabric8/crd/alt/LabelSelector.java deleted file mode 100644 index 7443b4fe3f8..00000000000 --- a/crd-generator/api/src/test/java/io/fabric8/crd/alt/LabelSelector.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.alt; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ ElementType.FIELD }) -@Retention(RetentionPolicy.RUNTIME) -public @interface LabelSelector { - -} diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/alt/SpecReplicas.java b/crd-generator/api/src/test/java/io/fabric8/crd/alt/SpecReplicas.java deleted file mode 100644 index 04f7e4049b7..00000000000 --- a/crd-generator/api/src/test/java/io/fabric8/crd/alt/SpecReplicas.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.alt; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ ElementType.FIELD }) -@Retention(RetentionPolicy.RUNTIME) -public @interface SpecReplicas { - -} diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/alt/StatusReplicas.java b/crd-generator/api/src/test/java/io/fabric8/crd/alt/StatusReplicas.java deleted file mode 100644 index 9b5428d8428..00000000000 --- a/crd-generator/api/src/test/java/io/fabric8/crd/alt/StatusReplicas.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.alt; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ ElementType.FIELD }) -@Retention(RetentionPolicy.RUNTIME) -public @interface StatusReplicas { - -} diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicListSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicListSpec.java index cbbcaa13f53..661a1892c8c 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicListSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicListSpec.java @@ -15,8 +15,11 @@ */ package io.fabric8.crd.example.cyclic; +import lombok.Data; + import java.util.List; +@Data public class CyclicListSpec { private List ref; } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicSpec.java index 56e19c2a379..49dc70a1711 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicSpec.java @@ -15,6 +15,9 @@ */ package io.fabric8.crd.example.cyclic; +import lombok.Data; + +@Data public class CyclicSpec { private Ref ref; } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicStatus.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicStatus.java index 804ea9937d6..59684dc6f35 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicStatus.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/CyclicStatus.java @@ -15,6 +15,9 @@ */ package io.fabric8.crd.example.cyclic; +import lombok.Data; + +@Data public class CyclicStatus { private String message; } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/Ref.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/Ref.java index 26c8e4c70be..025b3ebcdc7 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/Ref.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/Ref.java @@ -15,6 +15,9 @@ */ package io.fabric8.crd.example.cyclic; +import lombok.Data; + +@Data public class Ref { private Ref ref; diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/RefList.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/RefList.java index 73f7adf92c8..f2ea897c377 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/RefList.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/cyclic/RefList.java @@ -15,8 +15,11 @@ */ package io.fabric8.crd.example.cyclic; +import lombok.Data; + import java.util.List; +@Data public class RefList { private List ref; diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CollectionCyclicSchemaSwap.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CollectionCyclicSchemaSwap.java index 186449a4707..c8ecaeb71ca 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CollectionCyclicSchemaSwap.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CollectionCyclicSchemaSwap.java @@ -25,16 +25,16 @@ public class CollectionCyclicSchemaSwap extends CustomResource { public static class Spec { - private MyObject myObject; - private List levels; + public MyObject myObject; + public List levels; } public static class Level { - private MyObject myObject; - private List levels; + public MyObject myObject; + public List levels; } public static class MyObject { - private int value; + public int value; } } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CyclicSchemaSwap.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CyclicSchemaSwap.java index 19e672b5eac..83c2a83b4fa 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CyclicSchemaSwap.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CyclicSchemaSwap.java @@ -24,17 +24,17 @@ public class CyclicSchemaSwap extends CustomResource { public static class Spec { - private MyObject myObject; - private Level root; - private List roots; // should not interfere with the rendering depth of level of its sibling + public MyObject myObject; + public Level root; + public List roots; // should not interfere with the rendering depth of level of its sibling } public static class Level { - private MyObject myObject; - private Level level; + public MyObject myObject; + public Level level; } public static class MyObject { - private int value; + public int value; } } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/DeeplyNestedSchemaSwaps.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/DeeplyNestedSchemaSwaps.java index 617a1221bf5..d2824cd0a7c 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/DeeplyNestedSchemaSwaps.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/DeeplyNestedSchemaSwaps.java @@ -22,28 +22,28 @@ public class DeeplyNestedSchemaSwaps extends CustomResource { public static class Spec { - private MyObject myObject; - private Level1 level1; + public MyObject myObject; + public Level1 level1; } private static class Level1 { - private Level2 level2a; - private MyObject myObject; - private Level2 level2b; + public Level2 level2a; + public MyObject myObject; + public Level2 level2b; } private static class Level2 { - private MyObject myObject1; - private Level3 level3; - private MyObject myObject2; + public MyObject myObject1; + public Level3 level3; + public MyObject myObject2; } private static class Level3 { - private MyObject myObject1; - private MyObject myObject2; + public MyObject myObject1; + public MyObject myObject2; } public static class MyObject { - private int shouldBeString; + public int shouldBeString; } } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/ExtractionSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/ExtractionSpec.java index 678803dbf1a..ba671f77dd9 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/ExtractionSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/ExtractionSpec.java @@ -21,9 +21,9 @@ public class ExtractionSpec { @SchemaFrom(type = FooExtractor.class) - private Foo foo; + public Foo foo; @PreserveUnknownFields - private Foo bar; + public Foo bar; } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/NestedSchemaSwap.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/NestedSchemaSwap.java index 759b972e3de..77e9f00b3f3 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/NestedSchemaSwap.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/NestedSchemaSwap.java @@ -22,16 +22,16 @@ public class NestedSchemaSwap extends CustomResource unsupported; private Map supported; diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidationSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidationSpec.java index f233f69a4cc..c9b5b31ab90 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidationSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidationSpec.java @@ -17,7 +17,9 @@ import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; +import lombok.Data; +@Data @ValidationRule(value = "self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas", fieldPath = ".replicas") public class K8sValidationSpec { @Required @@ -82,6 +84,7 @@ enum Priority { high } + @Data static class DeepLevel1 { @Required private String valueL1; @@ -90,6 +93,7 @@ static class DeepLevel1 { private DeepLevel2 deepLevel2; } + @Data static class DeepLevel2 { @Required private String valueL2; @@ -99,6 +103,7 @@ static class DeepLevel2 { } + @Data @ValidationRule("self.dummy.startsWith('on-class-')") static class OnClass { @Required @@ -109,6 +114,7 @@ static class ClassWithValidationsFromAbstractClass extends AbstractBase { } + @Data @ValidationRule("self.dummy.startsWith('abstract-')") static abstract class AbstractBase { @Required diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidationStatus.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidationStatus.java index e038b893874..49fd5a7cb9f 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidationStatus.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidationStatus.java @@ -15,6 +15,9 @@ */ package io.fabric8.crd.example.k8svalidation; +import lombok.Data; + +@Data public class K8sValidationStatus { Integer availableReplicas; } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/map/ContainingMaps.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/map/ContainingMaps.java index 514e85647c7..26f00a82f6f 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/map/ContainingMaps.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/map/ContainingMaps.java @@ -18,9 +18,11 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Version; +import lombok.Data; import java.util.EnumMap; +@Data @Group("map.fabric8.io") @Version("v1alpha1") public class ContainingMaps extends CustomResource { diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/map/ContainingMapsSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/map/ContainingMapsSpec.java index ded900d8693..f1c4d06b5c2 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/map/ContainingMapsSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/map/ContainingMapsSpec.java @@ -15,10 +15,13 @@ */ package io.fabric8.crd.example.map; +import lombok.Data; + import java.util.HashMap; import java.util.List; import java.util.Map; +@Data public class ContainingMapsSpec { private Map> test = null; diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/person/Address.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/person/Address.java index 3cac71d5ee3..a638fb6b1fa 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/person/Address.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/person/Address.java @@ -16,11 +16,11 @@ package io.fabric8.crd.example.person; public class Address { - private String street; - private int number; - private String zip; - private String country; - private Type type; + public String street; + public int number; + public String zip; + public String country; + public Type type; public enum Type { home, diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/person/Person.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/person/Person.java index e036c400040..625a6302b92 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/person/Person.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/person/Person.java @@ -20,13 +20,13 @@ public class Person { - private String firstName; - private Optional middleName; - private String lastName; - private int birthYear; - private List hobbies; - private AddressList addresses; - private Type type; + public String firstName; + public Optional middleName; + public String lastName; + public int birthYear; + public List hobbies; + public AddressList addresses; + public Type type; public enum Type { crazy, diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerSpec.java index 79e62dd47ea..97140f77c57 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerSpec.java @@ -15,8 +15,10 @@ */ package io.fabric8.crd.example.webserver; -import io.fabric8.crd.alt.SpecReplicas; +import io.fabric8.kubernetes.model.annotation.SpecReplicas; +import lombok.Data; +@Data public class WebServerSpec { private int port; diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerStatus.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerStatus.java index 31cf2d32602..04ecf618206 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerStatus.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerStatus.java @@ -15,8 +15,10 @@ */ package io.fabric8.crd.example.webserver; -import io.fabric8.crd.alt.StatusReplicas; +import io.fabric8.kubernetes.model.annotation.StatusReplicas; +import lombok.Data; +@Data public class WebServerStatus { @StatusReplicas diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerWithSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerWithSpec.java index fefe2649321..5e08052d105 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerWithSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerWithSpec.java @@ -15,6 +15,9 @@ */ package io.fabric8.crd.example.webserver; +import lombok.Data; + +@Data public class WebServerWithSpec { private String name; diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerWithStatusProperty.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerWithStatusProperty.java index 8b25cc8339e..a73a4169bf0 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerWithStatusProperty.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/webserver/WebServerWithStatusProperty.java @@ -15,8 +15,10 @@ */ package io.fabric8.crd.example.webserver; -import io.fabric8.crd.alt.SpecReplicas; +import io.fabric8.kubernetes.model.annotation.SpecReplicas; +import lombok.Data; +@Data public class WebServerWithStatusProperty { private String name; diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/CRDGeneratorAssertions.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/CRDGeneratorAssertions.java index 173d31abff8..643d00c7f28 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/CRDGeneratorAssertions.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/CRDGeneratorAssertions.java @@ -121,6 +121,7 @@ public static void assertFileEquals(final File expectedFile, final File actualFi // skip license headers String expectedLine = skipCommentsAndEmptyLines(expectedReader); String actualLine = skipCommentsAndEmptyLines(actualReader); + // compare both files final String message = String.format("Expected %s and actual %s files are not equal", expectedFile, actualFile); while (expectedLine != null || actualLine != null) { diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/CRDGeneratorTest.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/CRDGeneratorTest.java index 97b089310eb..5bfcc7a1f3c 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/CRDGeneratorTest.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/CRDGeneratorTest.java @@ -16,32 +16,19 @@ package io.fabric8.crd.generator; import io.fabric8.crd.example.basic.Basic; -import io.fabric8.crd.example.basic.BasicSpec; -import io.fabric8.crd.example.basic.BasicStatus; import io.fabric8.crd.example.complex.Complex; import io.fabric8.crd.example.cyclic.Cyclic; import io.fabric8.crd.example.cyclic.CyclicList; import io.fabric8.crd.example.deprecated.v2.DeprecationExample; -import io.fabric8.crd.example.inherited.BaseSpec; -import io.fabric8.crd.example.inherited.BaseStatus; import io.fabric8.crd.example.inherited.Child; -import io.fabric8.crd.example.inherited.ChildSpec; -import io.fabric8.crd.example.inherited.ChildStatus; import io.fabric8.crd.example.joke.Joke; import io.fabric8.crd.example.joke.JokeRequest; -import io.fabric8.crd.example.joke.JokeRequestSpec; -import io.fabric8.crd.example.joke.JokeRequestStatus; import io.fabric8.crd.example.k8svalidation.K8sValidation; import io.fabric8.crd.example.map.ContainingMaps; -import io.fabric8.crd.example.map.ContainingMapsSpec; import io.fabric8.crd.example.multiple.v1.Multiple; -import io.fabric8.crd.example.multiple.v1.MultipleSpec; import io.fabric8.crd.example.nocyclic.NoCyclic; import io.fabric8.crd.example.simplest.Simplest; -import io.fabric8.crd.example.simplest.SimplestSpec; -import io.fabric8.crd.example.simplest.SimplestStatus; import io.fabric8.crd.generator.CRDGenerator.AbstractCRDOutput; -import io.fabric8.crd.generator.utils.Types; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceColumnDefinition; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionNames; @@ -64,7 +51,6 @@ import java.net.URL; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -212,12 +198,6 @@ void shouldProperlyGenerateMultipleVersionsOfCRDs() { assertEquals(2, versions.size()); assertEquals(1, versions.stream().filter(v -> v.getName().equals("v1")).count()); assertEquals(1, versions.stream().filter(v -> v.getName().equals("v2")).count()); - - Class[] mustContainTraversedClasses = { Multiple.class, MultipleSpec.class, - io.fabric8.crd.example.multiple.v2.Multiple.class, io.fabric8.crd.example.multiple.v2.MultipleSpec.class }; - final Set dependentClassNames = infos.get(specVersion).getDependentClassNames(); - Arrays.stream(mustContainTraversedClasses).map(Class::getCanonicalName) - .forEach(c -> assertTrue(dependentClassNames.contains(c), "should contain " + c)); } @Test @@ -329,8 +309,7 @@ private void outputCRDIfFailed(Class> customResou try { test.test(customResource); } catch (AssertionFailedError e) { - // output type def and crd - Types.output(output.get(keyFor(customResource)).definition()); + // output crd output.outputCRD(customResource); throw e; } @@ -340,7 +319,7 @@ private void outputCRDIfFailed(Class> customResou void simplestCRDShouldWork() { outputCRDIfFailed(Simplest.class, (customResource) -> { final CustomResourceDefinitionVersion version = checkCRD(customResource, "Simplest", "simplests", - Scope.CLUSTER, SimplestSpec.class, SimplestStatus.class); + Scope.CLUSTER); assertNotNull(version.getSubresources()); }); @@ -350,7 +329,7 @@ void simplestCRDShouldWork() { void inheritedCRDShouldWork() { outputCRDIfFailed(Child.class, (customResource) -> { final CustomResourceDefinitionVersion version = checkCRD(customResource, "Child", "children", - Scope.NAMESPACED, ChildSpec.class, ChildStatus.class, BaseSpec.class, BaseStatus.class); + Scope.NAMESPACED); assertNotNull(version.getSubresources()); final Map specProps = version.getSchema().getOpenAPIV3Schema() .getProperties().get("spec").getProperties(); @@ -366,7 +345,7 @@ void inheritedCRDShouldWork() { void mapPropertyShouldHaveCorrectValueType() { outputCRDIfFailed(ContainingMaps.class, (customResource) -> { final CustomResourceDefinitionVersion version = checkCRD(customResource, "ContainingMaps", "containingmaps", - Scope.CLUSTER, ContainingMapsSpec.class); + Scope.CLUSTER); assertNotNull(version.getSchema()); final Map specProps = version.getSchema().getOpenAPIV3Schema() @@ -412,9 +391,7 @@ void jokeCRDShouldWork() { @Test void jokerequestCRDShouldWork() { outputCRDIfFailed(JokeRequest.class, (customResource) -> { - final CustomResourceDefinitionSpec spec = checkSpec(customResource, Scope.NAMESPACED, - JokeRequestSpec.class, JokeRequestStatus.class, JokeRequestSpec.Category.class, JokeRequestSpec.ExcludedTopic.class, - JokeRequestStatus.State.class); + final CustomResourceDefinitionSpec spec = checkSpec(customResource, Scope.NAMESPACED); final CustomResourceDefinitionNames names = checkNames("JokeRequest", "jokerequests", spec); @@ -464,7 +441,7 @@ void jokerequestCRDShouldWork() { void checkCRDGenerator() { outputCRDIfFailed(Basic.class, (customResource) -> { final CustomResourceDefinitionVersion version = checkCRD(customResource, "Basic", "basics", - Scope.NAMESPACED, BasicSpec.class, BasicStatus.class); + Scope.NAMESPACED); assertNotNull(version.getSubresources()); CustomResourceValidation schema = version.getSchema(); assertNotNull(schema); @@ -577,8 +554,8 @@ void checkK8sValidationRules() throws Exception { private CustomResourceDefinitionVersion checkCRD(Class> customResource, String kind, String plural, - Scope scope, Class... traversedClasses) { - CustomResourceDefinitionSpec spec = checkSpec(customResource, scope, traversedClasses); + Scope scope) { + CustomResourceDefinitionSpec spec = checkSpec(customResource, scope); checkNames(kind, plural, spec); return checkVersion(spec); @@ -599,7 +576,7 @@ private CustomResourceDefinitionNames checkNames(String kind, String plural, Cus } private CustomResourceDefinitionSpec checkSpec( - Class> customResource, Scope scope, Class... mustContainTraversedClasses) { + Class> customResource, Scope scope) { CRDGenerator generator = newCRDGenerator(); // record info to be able to output it if the test fails @@ -619,11 +596,6 @@ private CustomResourceDefinitionSpec checkSpec( assertEquals(crdName, crdInfo.getCrdName()); assertEquals(v1, crdInfo.getCrdSpecVersion()); assertTrue(crdInfo.getFilePath().endsWith(CRDGenerator.getOutputName(crdName, v1))); // test output uses the CRD name as URI - if (mustContainTraversedClasses != null && mustContainTraversedClasses.length > 0) { - final Set dependentClassNames = crdInfo.getDependentClassNames(); - Arrays.stream(mustContainTraversedClasses).map(Class::getCanonicalName) - .forEach(c -> assertTrue(dependentClassNames.contains(c), "should contain " + c)); - } CustomResourceDefinition definition = output.definition(outputName); assertNotNull(definition); diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/utils/TypesTest.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/utils/TypesTest.java index a1c7451a1dd..74f34a64235 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/utils/TypesTest.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/utils/TypesTest.java @@ -15,85 +15,19 @@ */ package io.fabric8.crd.generator.utils; -import io.fabric8.crd.example.basic.Basic; -import io.fabric8.crd.example.basic.BasicSpec; -import io.fabric8.crd.example.basic.BasicStatus; import io.fabric8.crd.example.inherited.Child; -import io.fabric8.crd.example.joke.Joke; -import io.fabric8.crd.example.person.Person; -import io.fabric8.crd.example.webserver.WebServerWithStatusProperty; import io.fabric8.crd.generator.utils.Types.SpecAndStatus; -import io.sundr.model.ClassRef; -import io.sundr.model.Property; -import io.sundr.model.TypeDef; -import io.sundr.model.TypeRef; import org.junit.jupiter.api.Test; -import java.util.List; -import java.util.Optional; - import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; public class TypesTest { - @Test - void shouldFindStatusProperty() { - TypeDef def = Types.typeDefFrom(WebServerWithStatusProperty.class); - Optional p = Types.findStatusProperty(def); - assertTrue(p.isPresent()); - - def = Types.typeDefFrom(Basic.class); - p = Types.findStatusProperty(def); - assertTrue(p.isPresent()); - } - @Test void shouldFindInheritedStatusProperty() { - final TypeDef def = Types.typeDefFrom(Child.class); - final Optional p = Types.findStatusProperty(def); - assertTrue(p.isPresent()); - final Property property = p.get(); - final TypeRef typeRef = property.getTypeRef(); - assertTrue(typeRef instanceof ClassRef); - final ClassRef classRef = (ClassRef) typeRef; - final SpecAndStatus specAndStatus = Types.resolveSpecAndStatusTypes(def); - assertEquals(specAndStatus.getStatusClassName(), classRef.getFullyQualifiedName()); - } - - @Test - void shouldHaveAllTheExpectedProperties() { - final TypeDef def = Types.typeDefFrom(Joke.class); - final List properties = def.getProperties(); - assertEquals(7, properties.size()); + final SpecAndStatus specAndStatus = Types.resolveSpecAndStatusTypes(Child.class); + assertEquals("io.fabric8.crd.example.inherited.ChildStatus", specAndStatus.getStatusClassName()); + assertEquals("io.fabric8.crd.example.inherited.ChildSpec", specAndStatus.getSpecClassName()); } - @Test - void findingSuperClassesShouldWork() { - List superClasses = Types.typeDefFrom(Basic.class).getExtendsList(); - assertTrue(superClasses.stream().anyMatch(c -> c.getName().contains("CustomResource"))); - } - - @Test - void projectSuperClassesShouldProduceProperlyTypedClasses() { - List superClasses = Types.typeDefFrom(Basic.class).getExtendsList(); - assertEquals(2, superClasses.size()); - Optional crOpt = superClasses.stream() - .filter(c -> c.getName().contains("CustomResource")).findFirst(); - assertTrue(crOpt.isPresent()); - ClassRef crDef = crOpt.get(); - List arguments = crDef.getArguments(); - assertEquals(2, arguments.size()); - assertTrue(arguments.get(0).toString().contains(BasicSpec.class.getSimpleName())); - assertTrue(arguments.get(1).toString().contains(BasicStatus.class.getSimpleName())); - } - - @Test - void isNamespacedShouldWork() { - TypeDef def = Types.typeDefFrom(Basic.class); - assertTrue(Types.isNamespaced(def)); - def = Types.typeDefFrom(Person.class); - assertFalse(Types.isNamespaced(def)); - } } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java index a26a1427e7c..d6e67789c4c 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java @@ -15,8 +15,15 @@ */ package io.fabric8.crd.generator.v1; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.fabric8.crd.example.annotated.Annotated; import io.fabric8.crd.example.basic.Basic; import io.fabric8.crd.example.extraction.CollectionCyclicSchemaSwap; @@ -29,14 +36,18 @@ import io.fabric8.crd.example.extraction.NestedSchemaSwap; import io.fabric8.crd.example.json.ContainingJson; import io.fabric8.crd.example.person.Person; -import io.fabric8.crd.generator.utils.Types; +import io.fabric8.crd.generator.annotation.SchemaSwap; import io.fabric8.kubernetes.api.model.AnyType; +import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; import io.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRule; -import io.sundr.model.TypeDef; +import io.fabric8.kubernetes.api.model.coordination.v1.LeaseSpec; import org.junit.jupiter.api.Test; +import org.w3c.dom.Node; +import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -52,18 +63,15 @@ class JsonSchemaTest { @Test - void shouldCreatAnyTypeWithoutProperties() { - TypeDef any = Types.typeDefFrom(AnyType.class); - JSONSchemaProps schema = JsonSchema.from(any); - assertNotNull(any); + void shouldCreateAnyTypeWithoutProperties() { + JSONSchemaProps schema = JsonSchema.from(AnyType.class); assertSchemaHasNumberOfProperties(schema, 0); assertTrue(schema.getXKubernetesPreserveUnknownFields()); } @Test void shouldCreateJsonSchemaFromClass() { - TypeDef person = Types.typeDefFrom(Person.class); - JSONSchemaProps schema = JsonSchema.from(person); + JSONSchemaProps schema = JsonSchema.from(Person.class); assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 7); final List personTypes = properties.get("type").getEnum().stream().map(JsonNode::asText) @@ -81,8 +89,7 @@ void shouldCreateJsonSchemaFromClass() { assertTrue(addressTypes.contains("home")); assertTrue(addressTypes.contains("work")); - final TypeDef def = Types.typeDefFrom(Basic.class); - schema = JsonSchema.from(def); + schema = JsonSchema.from(Basic.class); assertNotNull(schema); properties = schema.getProperties(); assertNotNull(properties); @@ -98,8 +105,7 @@ void shouldCreateJsonSchemaFromClass() { @Test void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingException { - TypeDef annotated = Types.typeDefFrom(Annotated.class); - JSONSchemaProps schema = JsonSchema.from(annotated); + JSONSchemaProps schema = JsonSchema.from(Annotated.class); assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); final JSONSchemaProps specSchema = properties.get("spec"); @@ -210,8 +216,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti @Test void shouldProduceKubernetesPreserveFields() { - TypeDef containingJson = Types.typeDefFrom(ContainingJson.class); - JSONSchemaProps schema = JsonSchema.from(containingJson); + JSONSchemaProps schema = JsonSchema.from(ContainingJson.class); assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); final JSONSchemaProps specSchema = properties.get("spec"); @@ -239,8 +244,7 @@ void shouldProduceKubernetesPreserveFields() { @Test void shouldExtractPropertiesSchemaFromExtractValueAnnotation() { - TypeDef extraction = Types.typeDefFrom(Extraction.class); - JSONSchemaProps schema = JsonSchema.from(extraction); + JSONSchemaProps schema = JsonSchema.from(Extraction.class); assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); final JSONSchemaProps specSchema = properties.get("spec"); @@ -274,8 +278,7 @@ void shouldExtractPropertiesSchemaFromExtractValueAnnotation() { @Test void shouldExtractPropertiesSchemaFromSchemaSwapAnnotations() { - TypeDef extraction = Types.typeDefFrom(MultipleSchemaSwaps.class); - JSONSchemaProps schema = JsonSchema.from(extraction); + JSONSchemaProps schema = JsonSchema.from(MultipleSchemaSwaps.class); assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); final JSONSchemaProps specSchema = properties.get("spec"); @@ -302,8 +305,7 @@ void shouldExtractPropertiesSchemaFromSchemaSwapAnnotations() { @Test void shouldApplySchemaSwapsMultipleTimesInDeepClassHierarchy() { - TypeDef extraction = Types.typeDefFrom(DeeplyNestedSchemaSwaps.class); - JSONSchemaProps schema = JsonSchema.from(extraction); + JSONSchemaProps schema = JsonSchema.from(DeeplyNestedSchemaSwaps.class); assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); Map spec = assertSchemaHasNumberOfProperties(properties.get("spec"), 2); @@ -328,8 +330,7 @@ void shouldApplySchemaSwapsMultipleTimesInDeepClassHierarchy() { @Test void shouldApplyCyclicSchemaSwaps() { - TypeDef extraction = Types.typeDefFrom(CyclicSchemaSwap.class); - JSONSchemaProps schema = JsonSchema.from(extraction); + JSONSchemaProps schema = JsonSchema.from(CyclicSchemaSwap.class); assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); @@ -344,10 +345,157 @@ void shouldApplyCyclicSchemaSwaps() { assertNull(spec.get("root").getProperties().get("level").getProperties().get("level")); } + public static class PreserveUnknown { + + @JsonIgnore + private Map values = new HashMap<>(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.values; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.values.put(name, value); + } + + public Map getValues() { + return values; + } + + public void setValues(Map values) { + this.values = values; + } + + } + + @Test + void testPreserveUnknown() { + JSONSchemaProps schema = JsonSchema.from(PreserveUnknown.class); + assertNotNull(schema); + assertEquals(0, schema.getProperties().size()); + assertEquals(Boolean.TRUE, schema.getXKubernetesPreserveUnknownFields()); + } + + // implicitly uses AnySchema + private static class MySerializer extends StdSerializer { + + public MySerializer() { + super(Object.class); + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { + + } + + } + + public static class AnyTestTypes { + @JsonSerialize(using = MySerializer.class) + public Object value; + public Node nodeValue; + } + + @Test + void testAnySchemaTypes() { + JSONSchemaProps schema = JsonSchema.from(AnyTestTypes.class); + assertNotNull(schema); + assertNull(schema.getProperties().get("nodeValue").getType()); + assertEquals(Boolean.TRUE, schema.getProperties().get("nodeValue").getXKubernetesPreserveUnknownFields()); + assertEquals(Boolean.TRUE, schema.getProperties().get("value").getXKubernetesPreserveUnknownFields()); + } + + @Test + void testLeaseSpec() { + JSONSchemaProps schema = JsonSchema.from(LeaseSpec.class); + assertNotNull(schema); + JSONSchemaProps renewTime = schema.getProperties().get("renewTime"); + assertEquals("string", renewTime.getType()); + assertEquals("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", renewTime.getPattern()); + assertEquals("date-time", renewTime.getFormat()); + } + + @Test + void testServicePort() { + JSONSchemaProps schema = JsonSchema.from(ServicePort.class); + assertNotNull(schema); + JSONSchemaProps targetPort = schema.getProperties().get("targetPort"); + assertEquals(2, targetPort.getAnyOf().size()); + assertEquals(true, targetPort.getXKubernetesIntOrString()); + } + + private static class MapProperty { + + public Map map; + + } + + @Test + void testMapProperty() { + JSONSchemaProps schema = JsonSchema.from(MapProperty.class); + assertNotNull(schema); + JSONSchemaProps map = schema.getProperties().get("map"); + assertTrue(map.getProperties().isEmpty()); + assertEquals("boolean", map.getAdditionalProperties().getSchema().getType()); + } + + private static class Cyclic1 { + + public Cyclic1 parent; + + } + + private static class Cyclic2 { + + public Cyclic2 parent[]; + + } + + private static class Cyclic3 { + + public Map parent; + + } + + @Test + void testCyclicProperties() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> JsonSchema.from(Cyclic1.class)); + assertEquals("Found a cyclic reference involving the field of type io.fabric8.crd.generator.v1.JsonSchemaTest$Cyclic1 starting a field parent >>\n" + + "io.fabric8.crd.generator.v1.JsonSchemaTest$Cyclic1.parent", exception.getMessage()); + + exception = assertThrows(IllegalArgumentException.class, + () -> JsonSchema.from(Cyclic2.class)); + assertEquals("Found a cyclic reference involving the field of type io.fabric8.crd.generator.v1.JsonSchemaTest$Cyclic2 starting a field parent >>\n" + + "io.fabric8.crd.generator.v1.JsonSchemaTest$Cyclic2.parent", exception.getMessage()); + + exception = assertThrows(IllegalArgumentException.class, + () -> JsonSchema.from(Cyclic3.class)); + assertEquals("Found a cyclic reference involving the field of type io.fabric8.crd.generator.v1.JsonSchemaTest$Cyclic3 starting a field parent >>\n" + + "io.fabric8.crd.generator.v1.JsonSchemaTest$Cyclic3.parent", exception.getMessage()); + } + + @SchemaSwap(originalType = Cyclic3.class, fieldName = "parent") + private static class Cyclic4 { + + public Cyclic3 parent; + public int value; + + } + + @Test + void testSchemaSwapZeroDepth() { + JSONSchemaProps schema = JsonSchema.from(Cyclic4.class); + assertNotNull(schema); + JSONSchemaProps parent = schema.getProperties().get("parent"); + assertTrue(parent.getProperties().isEmpty()); + } + @Test void shouldApplyCollectionCyclicSchemaSwaps() { - TypeDef extraction = Types.typeDefFrom(CollectionCyclicSchemaSwap.class); - JSONSchemaProps schema = JsonSchema.from(extraction); + JSONSchemaProps schema = JsonSchema.from(CollectionCyclicSchemaSwap.class); assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); @@ -373,9 +521,8 @@ void shouldApplyCollectionCyclicSchemaSwaps() { @Test void shouldThrowIfSchemaSwapHasUnmatchedField() { - TypeDef incorrectExtraction = Types.typeDefFrom(IncorrectExtraction.class); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> JsonSchema.from(incorrectExtraction)); + () -> JsonSchema.from(IncorrectExtraction.class)); assertEquals( "Unmatched SchemaSwaps: @SchemaSwap(originalType=io.fabric8.crd.example.extraction.ExtractionSpec, fieldName=\"FOO\", targetType=io" + ".fabric8.crd.example.extraction.FooExtractor) on io.fabric8.crd.example.extraction.IncorrectExtraction", @@ -384,9 +531,8 @@ void shouldThrowIfSchemaSwapHasUnmatchedField() { @Test void shouldThrowIfSchemaSwapHasUnmatchedClass() { - TypeDef incorrectExtraction2 = Types.typeDefFrom(IncorrectExtraction2.class); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> JsonSchema.from(incorrectExtraction2)); + () -> JsonSchema.from(IncorrectExtraction2.class)); assertEquals( "Unmatched SchemaSwaps: @SchemaSwap(originalType=io.fabric8.crd.example.basic.BasicSpec, fieldName=\"bar\", targetType=io.fabric8.crd" + ".example.extraction.FooExtractor) on io.fabric8.crd.example.extraction.IncorrectExtraction2", @@ -395,15 +541,31 @@ void shouldThrowIfSchemaSwapHasUnmatchedClass() { @Test void shouldThrowIfSchemaSwapNested() { - TypeDef nested = Types.typeDefFrom(NestedSchemaSwap.class); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> JsonSchema.from(nested)); + () -> JsonSchema.from(NestedSchemaSwap.class)); assertEquals( - "Nested SchemaSwap: @SchemaSwap(originalType=io.fabric8.crd.example.extraction.NestedSchemaSwap.End, fieldName=\"value\", targetType=java.lang.Void) " - + "on io.fabric8.crd.example.extraction.NestedSchemaSwap.Intermediate", + "Nested SchemaSwap: @SchemaSwap(originalType=io.fabric8.crd.example.extraction.NestedSchemaSwap$End, fieldName=\"value\", targetType=java.lang.Void) " + + "on io.fabric8.crd.example.extraction.NestedSchemaSwap$Intermediate", exception.getMessage()); } + @io.fabric8.generator.annotation.ValidationRule(value = "base", messageExpression = "something", reason = "FieldValueForbidden") + private static class Base { + public String value; + } + + @io.fabric8.generator.annotation.ValidationRule(value = "parent", messageExpression = "something else", reason = "FieldValueForbidden") + private static class Parent extends Base { + + } + + @Test + void testValidationRuleHierarchy() { + JSONSchemaProps schema = JsonSchema.from(Parent.class); + assertNotNull(schema); + assertEquals(2, schema.getXKubernetesValidations().size()); + } + private static Map assertSchemaHasNumberOfProperties(JSONSchemaProps specSchema, int expected) { Map spec = specSchema.getProperties(); assertEquals(expected, spec.size()); diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/visitor/SpecReplicasPathDetectorTest.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/SpecReplicasPathTest.java similarity index 50% rename from crd-generator/api/src/test/java/io/fabric8/crd/generator/visitor/SpecReplicasPathDetectorTest.java rename to crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/SpecReplicasPathTest.java index 328de53cb2a..bd4d5a90f88 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/visitor/SpecReplicasPathDetectorTest.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/SpecReplicasPathTest.java @@ -13,39 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.fabric8.crd.generator.visitor; +package io.fabric8.crd.generator.v1; import io.fabric8.crd.example.webserver.WebServerWithSpec; import io.fabric8.crd.example.webserver.WebServerWithStatusProperty; -import io.sundr.adapter.api.AdapterContext; -import io.sundr.adapter.api.Adapters; -import io.sundr.model.TypeDef; -import io.sundr.model.TypeDefBuilder; -import io.sundr.model.repo.DefinitionRepository; +import io.fabric8.crd.generator.ResolvingContext; +import io.fabric8.kubernetes.model.annotation.SpecReplicas; import org.junit.jupiter.api.Test; +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -class SpecReplicasPathDetectorTest { - - public static final AdapterContext CONTEXT = AdapterContext.create(DefinitionRepository.getRepository()); +class SpecReplicasPathTest { @Test public void shoudDetectSpecReplicasPath() throws Exception { - TypeDef def = Adapters.adaptType(WebServerWithStatusProperty.class, CONTEXT); - SpecReplicasPathDetector detector = new SpecReplicasPathDetector(); - def = new TypeDefBuilder(def).accept(detector).build(); - assertTrue(detector.getPath().isPresent()); - assertEquals(".replicas", detector.getPath().get()); + JsonSchema resolver = new JsonSchema(ResolvingContext.defaultResolvingContext(), WebServerWithStatusProperty.class); + Optional path = resolver.getSinglePath(SpecReplicas.class); + assertTrue(path.isPresent()); + assertEquals(".replicas", path.get()); } @Test public void shoudDetectNestedSpecReplicasPath() throws Exception { - TypeDef def = Adapters.adaptType(WebServerWithSpec.class, CONTEXT); - SpecReplicasPathDetector detector = new SpecReplicasPathDetector(); - def = new TypeDefBuilder(def).accept(detector).build(); - assertTrue(detector.getPath().isPresent()); - assertEquals(".spec.replicas", detector.getPath().get()); + JsonSchema resolver = new JsonSchema(ResolvingContext.defaultResolvingContext(), WebServerWithSpec.class); + Optional path = resolver.getSinglePath(SpecReplicas.class); + assertTrue(path.isPresent()); + assertEquals(".spec.replicas", path.get()); } } diff --git a/crd-generator/apt/pom.xml b/crd-generator/apt/pom.xml deleted file mode 100644 index f53bd95e3ef..00000000000 --- a/crd-generator/apt/pom.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - crd-generator-parent - io.fabric8 - 6.13-SNAPSHOT - - 4.0.0 - - crd-generator-apt - Fabric8 :: CRD generator :: Annotation Processor - - - - io.fabric8 - crd-generator-api - ${project.version} - - - - - - maven-compiler-plugin - - - io.sundr.builder.internal.processor.BuildableProcessor - - - - - - diff --git a/crd-generator/apt/src/main/java/io/fabric8/crd/generator/apt/CustomResourceAnnotationProcessor.java b/crd-generator/apt/src/main/java/io/fabric8/crd/generator/apt/CustomResourceAnnotationProcessor.java deleted file mode 100644 index 217d06d01b7..00000000000 --- a/crd-generator/apt/src/main/java/io/fabric8/crd/generator/apt/CustomResourceAnnotationProcessor.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2015 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.fabric8.crd.generator.apt; - -import io.fabric8.crd.generator.CRDGenerationInfo; -import io.fabric8.crd.generator.CRDGenerator; -import io.fabric8.crd.generator.CRDGenerator.AbstractCRDOutput; -import io.fabric8.crd.generator.CustomResourceInfo; -import io.fabric8.crd.generator.annotation.Annotations; -import io.fabric8.crd.generator.annotation.Labels; -import io.fabric8.crd.generator.utils.Types; -import io.fabric8.crd.generator.utils.Types.SpecAndStatus; -import io.fabric8.kubernetes.api.Pluralize; -import io.fabric8.kubernetes.client.utils.Utils; -import io.fabric8.kubernetes.model.Scope; -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.ShortNames; -import io.fabric8.kubernetes.model.annotation.Singular; -import io.fabric8.kubernetes.model.annotation.Version; -import io.sundr.adapter.api.Adapters; -import io.sundr.adapter.apt.AptContext; -import io.sundr.model.TypeDef; -import io.sundr.model.repo.DefinitionRepository; - -import java.io.IOException; -import java.io.OutputStream; -import java.lang.annotation.Annotation; -import java.net.URI; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.Messager; -import javax.annotation.processing.ProcessingEnvironment; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; -import javax.lang.model.element.Name; -import javax.lang.model.element.TypeElement; -import javax.tools.Diagnostic; -import javax.tools.FileObject; -import javax.tools.StandardLocation; - -@SupportedAnnotationTypes({ "io.fabric8.kubernetes.model.annotation.Version" }) -public class CustomResourceAnnotationProcessor extends AbstractProcessor { - - public static final String PROCESSOR_OPTION_PARALLEL = "io.fabric8.crd.generator.parallel"; - private final CRDGenerator generator = new CRDGenerator(); - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latestSupported(); - } - - @SuppressWarnings("unchecked") - public boolean process(Set annotations, RoundEnvironment roundEnv) { - if (roundEnv.processingOver()) { - final Messager messager = processingEnv.getMessager(); - final CRDGenerator crdGenerator = generator - .withOutput(new FileObjectCRDOutput(processingEnv)); - final Map options = processingEnv.getOptions(); - enableParallelGeneration(messager, crdGenerator, options); - final CRDGenerationInfo allCRDs = crdGenerator - .detailedGenerate(); - allCRDs.getCRDDetailsPerNameAndVersion().forEach((crdName, versionToInfo) -> { - messager.printMessage(Diagnostic.Kind.NOTE, "Generating CRD " + crdName + ":\n"); - versionToInfo.forEach( - (version, info) -> messager.printMessage(Diagnostic.Kind.NOTE, " - " + version + " -> " + info.getFilePath())); - }); - return true; - } - - // make sure we create the context before using it - AptContext.create(processingEnv.getElementUtils(), processingEnv.getTypeUtils(), DefinitionRepository.getRepository()); - - //Collect all annotated types. - for (TypeElement annotation : annotations) { - for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { - if (element instanceof TypeElement) { - try { - // The annotation is loaded with reflection for compatibility with Java 8 - Class generatedAnnotation = (Class) Class.forName("javax.annotation.processing.Generated"); - if (element.getAnnotationsByType(generatedAnnotation).length > 0) { - continue; - } - } catch (ClassNotFoundException e) { - // ignore - } - generator.customResources(toCustomResourceInfo((TypeElement) element)); - } - } - } - - return false; - } - - private void enableParallelGeneration(Messager messager, CRDGenerator crdGenerator, Map options) { - if (options.containsKey(PROCESSOR_OPTION_PARALLEL)) { - final String rawValue = options.get(PROCESSOR_OPTION_PARALLEL); - final boolean parallel = Boolean.parseBoolean(rawValue); - messager.printMessage(Diagnostic.Kind.NOTE, - String.format("Found option %s set to %s, parallel set to %b ", PROCESSOR_OPTION_PARALLEL, rawValue, parallel)); - crdGenerator.withParallelGenerationEnabled(parallel); - } - } - - private CustomResourceInfo toCustomResourceInfo(TypeElement customResource) { - TypeDef definition = Adapters.adaptType(customResource, AptContext.getContext()); - definition = Types.unshallow(definition); - - if (CustomResourceInfo.DESCRIBE_TYPE_DEFS) { - Types.output(definition); - } - final Name crClassName = customResource.getQualifiedName(); - - SpecAndStatus specAndStatus = Types.resolveSpecAndStatusTypes(definition); - if (specAndStatus.isUnreliable()) { - System.out.println("Cannot reliably determine status types for " + crClassName - + " because it isn't parameterized with only spec and status types. Status replicas detection will be deactivated."); - } - - final String group = customResource.getAnnotation(Group.class).value(); - final String version = customResource.getAnnotation(Version.class).value(); - - final String kind = Optional.ofNullable(customResource.getAnnotation(Kind.class)) - .map(Kind::value) - .orElse(customResource.getSimpleName().toString()); - - final String singular = Optional.ofNullable(customResource.getAnnotation(Singular.class)) - .map(Singular::value) - .orElse(kind.toLowerCase(Locale.ROOT)); - - final String plural = Optional.ofNullable(customResource.getAnnotation(Plural.class)) - .map(Plural::value) - .map(s -> s.toLowerCase(Locale.ROOT)) - .orElse(Pluralize.toPlural(singular)); - - final String[] shortNames = Optional - .ofNullable(customResource.getAnnotation(ShortNames.class)) - .map(ShortNames::value) - .orElse(new String[] {}); - - final String[] annotations = Optional - .ofNullable(customResource.getAnnotation(Annotations.class)) - .map(Annotations::value) - .orElse(new String[] {}); - - final String[] labels = Optional - .ofNullable(customResource.getAnnotation(Labels.class)) - .map(Labels::value) - .orElse(new String[] {}); - - final boolean storage = customResource.getAnnotation(Version.class).storage(); - final boolean served = customResource.getAnnotation(Version.class).served(); - final boolean deprecated = customResource.getAnnotation(Version.class).deprecated(); - final String deprecationWarning = Optional.ofNullable(customResource.getAnnotation(Version.class).deprecationWarning()) - .filter(s -> deprecated) - .filter(Utils::isNotNullOrEmpty) - .orElse(null); - - final Scope scope = Types.isNamespaced(definition) ? Scope.NAMESPACED : Scope.CLUSTER; - - return new CustomResourceInfo(group, version, kind, singular, plural, shortNames, storage, served, - deprecated, deprecationWarning, - scope, definition, crClassName.toString(), - specAndStatus.getSpecClassName(), specAndStatus.getStatusClassName(), annotations, labels); - } - - private static class FileObjectOutputStream extends OutputStream { - - private final FileObject yml; - private final OutputStream out; - - public FileObjectOutputStream(FileObject yml) throws IOException { - this.yml = yml; - this.out = yml.openOutputStream(); - } - - @Override - public void write(byte[] b) throws IOException { - out.write(b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - out.write(b, off, len); - } - - @Override - public void flush() throws IOException { - out.flush(); - } - - @Override - public void close() throws IOException { - out.close(); - } - - @Override - public void write(int b) throws IOException { - out.write(b); - } - - public URI toUri() { - return yml.toUri(); - } - } - - private static class FileObjectCRDOutput extends AbstractCRDOutput { - - private final ProcessingEnvironment processingEnv; - - public FileObjectCRDOutput(ProcessingEnvironment processingEnv) { - this.processingEnv = processingEnv; - } - - @Override - protected FileObjectOutputStream createStreamFor(String crdName) throws IOException { - return new FileObjectOutputStream( - processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", - "META-INF/fabric8/" + crdName + ".yml")); - } - - @Override - public URI crdURI(String crdName) { - return getStreamFor(crdName).toUri(); - } - } -} diff --git a/crd-generator/apt/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/crd-generator/apt/src/main/resources/META-INF/services/javax.annotation.processing.Processor deleted file mode 100644 index 5fea7c934fc..00000000000 --- a/crd-generator/apt/src/main/resources/META-INF/services/javax.annotation.processing.Processor +++ /dev/null @@ -1 +0,0 @@ -io.fabric8.crd.generator.apt.CustomResourceAnnotationProcessor diff --git a/crd-generator/pom.xml b/crd-generator/pom.xml index c86434e7c0f..71dd33d0a43 100644 --- a/crd-generator/pom.xml +++ b/crd-generator/pom.xml @@ -31,19 +31,6 @@ Fabric8 :: CRD generator :: Parent - apt api - - - - tests - - (,17] - - - test - - - diff --git a/crd-generator/test/pom.xml b/crd-generator/test/pom.xml index 96240459e9e..a93d0ccda02 100644 --- a/crd-generator/test/pom.xml +++ b/crd-generator/test/pom.xml @@ -36,9 +36,9 @@ io.fabric8 - crd-generator-apt + crd-generator-api - + org.projectlombok lombok diff --git a/doc/CRD-generator.md b/doc/CRD-generator.md index 036a5466c9c..efc15b761c3 100644 --- a/doc/CRD-generator.md +++ b/doc/CRD-generator.md @@ -2,25 +2,18 @@ ## Quick start -Import the Annotation Processor into your build. +TBD with Maven: ```xml - - io.fabric8 - crd-generator-apt - provided - +TBD ``` with Gradle: ```groovy -dependencies { - annotationProcessor 'io.fabric8:crd-generator-apt:' - ... -} +TBD ``` Now you can define a `class` that extends `io.fabric8.kubernetes.client.CustomResource` diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/CustomResource.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/CustomResource.java index 5992ca2194a..0d9061925e9 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/CustomResource.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/CustomResource.java @@ -118,22 +118,22 @@ public CustomResource() { this.status = initStatus(); } - public static boolean getServed(Class clazz) { + public static boolean getServed(Class clazz) { final Version annotation = clazz.getAnnotation(Version.class); return annotation == null || annotation.served(); } - public static boolean getStorage(Class clazz) { + public static boolean getStorage(Class clazz) { final Version annotation = clazz.getAnnotation(Version.class); return annotation == null || annotation.storage(); } - public static boolean getDeprecated(Class clazz) { + public static boolean getDeprecated(Class clazz) { final Version annotation = clazz.getAnnotation(Version.class); return annotation == null || annotation.deprecated(); } - public static String getDeprecationWarning(Class clazz) { + public static String getDeprecationWarning(Class clazz) { final Version annotation = clazz.getAnnotation(Version.class); return annotation != null && Utils.isNotNullOrEmpty(annotation.deprecationWarning()) ? annotation.deprecationWarning() @@ -255,7 +255,7 @@ public String getCRDName() { * @param clazz the CustomResource class which short names we want to retrieve * @return the short names associated with this CustomResource or an empty array if none was provided */ - public static String[] getShortNames(Class clazz) { + public static String[] getShortNames(Class clazz) { return Optional.ofNullable(clazz.getAnnotation(ShortNames.class)) .map(ShortNames::value) .orElse(new String[] {}); diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesSerialization.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesSerialization.java index 609bf20a0ef..d3a00fa548f 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesSerialization.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesSerialization.java @@ -379,7 +379,7 @@ public UnmatchedFieldTypeModule getUnmatchedFieldTypeModule() { return unmatchedFieldTypeModule; } - ObjectMapper getMapper() { + protected ObjectMapper getMapper() { return mapper; } diff --git a/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/Duration.java b/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/Duration.java index 8f13b7000ce..ad6ff66d1c5 100644 --- a/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/Duration.java +++ b/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/Duration.java @@ -18,10 +18,13 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -178,12 +181,21 @@ public static Duration parse(String duration) throws ParseException { return new Duration(accumulator); } - public static class Serializer extends JsonSerializer { + public static class Serializer extends StdSerializer { + + public Serializer() { + super(Duration.class); + } @Override public void serialize(Duration duration, JsonGenerator jgen, SerializerProvider provider) throws IOException { jgen.writeString(String.format("%sns", duration.getValue())); } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { + visitor.expectStringFormat(typeHint); + } } private enum TimeUnits { diff --git a/kubernetes-tests/pom.xml b/kubernetes-tests/pom.xml index 394751c44a3..c7860771cf1 100644 --- a/kubernetes-tests/pom.xml +++ b/kubernetes-tests/pom.xml @@ -39,11 +39,6 @@ openshift-client - - io.fabric8 - crd-generator-apt - - org.junit.jupiter junit-jupiter-engine diff --git a/pom.xml b/pom.xml index 602b62a18e5..644830eb024 100644 --- a/pom.xml +++ b/pom.xml @@ -244,11 +244,6 @@ java-generator-core ${project.version} - - io.fabric8 - crd-generator-apt - ${project.version} - io.fabric8 crd-generator-api