From 0156d0fd9f7d8beed68625778e976487a0f4fafc Mon Sep 17 00:00:00 2001 From: Dusan Jakub Date: Tue, 13 Sep 2022 11:43:26 +0200 Subject: [PATCH] fix: Stack overflow on multimaps (and other non-trivial generic classes), schema for multimaps is wrong Logic in sundrio that is responsible for replacing generic type arguments on methods and properties with their concrete instantiations from subclasses contained a bug, in which it looped infinitely if the same type argument name was used at multiple classes in the hierarchy. A common occurence were multimaps. A very similar code was also present in crd-generator, also suffering from the same bug. Sundrio has been updated to account for this situation, and updated utilities from sundrio have been introduced to crd-generator, which replace the previous implementation of the same algorithm. A test has been added to validate that multimaps (like `class MultiMap implements Map>`) are generated correctly. Fixes fabric8io/kubernetes-client#4357 Fixes fabric8io/kubernetes-client#4487 --- CHANGELOG.md | 2 + .../io/fabric8/crd/generator/utils/Types.java | 122 +++++------------- .../crd/example/map/ContainingMapsSpec.java | 29 +++++ .../crd/generator/CRDGeneratorTest.java | 115 ++++++++++------- .../crd/generator/maps/ContainingMaps.java | 26 ++++ .../generator/maps/ContainingMapsCRDTest.java | 64 +++++++++ .../generator/maps/ContainingMapsSpec.java | 51 ++++++++ pom.xml | 2 +- 8 files changed, 271 insertions(+), 140 deletions(-) create mode 100644 crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMaps.java create mode 100644 crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMapsCRDTest.java create mode 100644 crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMapsSpec.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c1488ee2ed..3d09123087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Fix #4473: Fix regression in backoff interval introduced in #4365 * Fix #4478: Removing the resourceVersion bump with null status * Fix #4482: Fixing blocking behavior of okhttp log watch +* Fix #4487: Schema for multimaps is now generated correctly #### Improvements * Fix #4471: Adding KubernetesClientBuilder.withHttpClientBuilderConsumer to further customize the HttpClient for any implementation. @@ -30,6 +31,7 @@ * Fix #4243: Update Tekton triggers model to v0.20.2 * Fix #4383: bump snakeyaml from 1.30 to 1.31 * Fix #4347: Update Kubernetes Model to v1.25.0 +* Fix #4413: Update sundrio to 0.93.1 #### New Features * Fix #4398: add annotation @PreserveUnknownFields for marking generated field have `x-kubernetes-preserve-unknown-fields: true` defined 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 a4f9498f32..90909672be 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 @@ -25,6 +25,10 @@ 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; @@ -33,7 +37,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; - public class Types { private Types() { throw new IllegalStateException("Utility class"); @@ -45,7 +48,8 @@ private Types() { 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. + * 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(); @@ -62,10 +66,10 @@ public static TypeDef unshallow(TypeDef definition) { 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()); + 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) { @@ -73,87 +77,30 @@ public static TypeDef typeDefFrom(ClassRef classRef) { } private static TypeDef projectDefinition(ClassRef ref) { - List arguments = ref.getArguments(); TypeDef definition = GetDefinition.of(ref); - if (arguments.isEmpty()) { + 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(); } - - List parameters = definition.getParameters(); - Map mappings = new HashMap<>(); - for (int i = 0; i < arguments.size(); i++) { - String name = parameters.get(i).getName(); - TypeRef typeRef = arguments.get(i); - mappings.put(name, typeRef); - } - - return new TypeDefBuilder(definition) - .accept(mapClassRefArguments(mappings), mapGenericProperties(mappings)) - .build(); - } - - /** - * Map generic properties to known {@link TypeRef} based on the specified mappings. - * Example: Given a property {@code T size} and a map containing {@code T -> Integer} the final - * property will be: {@code Integer type}. - * @param mappings A map that maps class arguments names to types. - * @return a visitors that performs the actual mapping. - */ - private static TypedVisitor mapGenericProperties(Map mappings) { - return new TypedVisitor() { - @Override - public void visit(PropertyBuilder property) { - TypeRef typeRef = property.buildTypeRef(); - if (typeRef instanceof TypeParamRef) { - TypeParamRef typeParamRef = (TypeParamRef) typeRef; - String key = typeParamRef.getName(); - TypeRef paramRef = mappings.get(key); - if (paramRef != null) { - property.withTypeRef(paramRef); - } - } - } - }; - } - - /** - * Map arguments, of {@link ClassRef} instances to known {@link TypeRef} based on the specified mappings. - * Example: Given a class reference to {@code Shape} and a map containing {@code T -> Integer} - * the final reference will be: {@code Shape type}. - * @param mappings A map that maps class arguments names to types. - * @return a visitors that performs the actual mapping. - */ - private static TypedVisitor mapClassRefArguments(Map mappings) { - return new TypedVisitor() { - @Override - public void visit(ClassRefBuilder c) { - List arguments = new ArrayList<>(); - for (TypeRef arg : c.buildArguments()) { - TypeRef mappedRef = arg; - if (arg instanceof TypeParamRef) { - TypeParamRef typeParamRef = (TypeParamRef) arg; - TypeRef mapping = mappings.get(typeParamRef.getName()); - if (mapping != null) { - mappedRef = mapping; - } - } - arguments.add(mappedRef); - } - c.withArguments(arguments); - } - }; } 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()); + .flatMap(s -> Stream.concat(Stream.of(s), projectDefinition(s).getExtendsList().stream())) + .collect(Collectors.toSet()); } /** * All non-static properties (including inherited). - * @param typeDef The type. + * + * @param typeDef The type. * @return A list with all properties. */ private static List projectProperties(TypeDef typeDef) { @@ -172,23 +119,20 @@ private static List projectProperties(TypeDef typeDef) { return !p.isStatic(); }), typeDef.getExtendsList().stream() - .filter(e -> !e.getFullyQualifiedName().startsWith("java.")) - .flatMap(e -> projectProperties(projectDefinition(e)) - .stream() - .filter(p -> filterCustomResourceProperties(e).test(p))) - ) + .filter(e -> !e.getFullyQualifiedName().startsWith("java.")) + .flatMap(e -> projectProperties(projectDefinition(e)) + .stream() + .filter(p -> filterCustomResourceProperties(e).test(p)))) - .collect(Collectors.toList()); + .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"))); + (!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); @@ -258,7 +202,7 @@ public boolean isUnreliable() { public static SpecAndStatus resolveSpecAndStatusTypes(TypeDef definition) { Optional optionalCustomResourceRef = definition.getExtendsList().stream() - .filter(s -> s.getFullyQualifiedName().equals(CUSTOM_RESOURCE_NAME)).findFirst(); + .filter(s -> s.getFullyQualifiedName().equals(CUSTOM_RESOURCE_NAME)).findFirst(); if (optionalCustomResourceRef.isPresent()) { ClassRef customResourceRef = optionalCustomResourceRef.get(); List arguments = customResourceRef.getArguments(); @@ -309,14 +253,14 @@ public static boolean isNamespaced(TypeDef definition, Set visited) { */ public static Optional findStatusProperty(TypeDef typeDef) { return typeDef.getProperties().stream() - .filter(Types::isStatusProperty) - .findFirst(); + .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 */ 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 e09a7ee8f0..78069f5889 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,6 +15,7 @@ */ package io.fabric8.crd.example.map; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,4 +33,32 @@ public Map>> getTest2() { return test2; } + private MultiHashMap stringToIntMultiMap1; + private MultiMap stringToIntMultiMap2; + private SwappedParametersMap, String> stringToIntMultiMap3; + private RedundantParametersMap> stringToIntMultiMap4; + private RedundantParametersStringToIntMultiMap stringToIntMultiMap5; + private StringKeyedMultiHashMap stringToIntMultiMap6; + private IntValuedMultiMap stringToIntMultiMap7; + + static class MultiHashMap extends HashMap> { + } + + interface MultiMap extends Map> { + } + + interface SwappedParametersMap extends Map { + } + + interface RedundantParametersMap extends Map { + } + + interface RedundantParametersStringToIntMultiMap extends Map> { + } + + static class StringKeyedMultiHashMap extends MultiHashMap { + } + + interface IntValuedMultiMap extends MultiMap { + } } 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 a1f9636435..2e8c98c9fa 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 @@ -137,9 +137,9 @@ void shouldProperlyRecordNumberOfGeneratedCRDs() { assertEquals(0, generator.detailedGenerate().numberOfGeneratedCRDs()); final CRDGenerationInfo info = generator - .customResourceClasses(Simplest.class, Child.class, Joke.class, JokeRequest.class) - .forCRDVersions("v1", "v1beta1") - .withOutput(output).detailedGenerate(); + .customResourceClasses(Simplest.class, Child.class, Joke.class, JokeRequest.class) + .forCRDVersions("v1", "v1beta1") + .withOutput(output).detailedGenerate(); assertEquals(4 * 2, info.numberOfGeneratedCRDs()); final Map> details = info.getCRDDetailsPerNameAndVersion(); @@ -160,9 +160,9 @@ void shouldProperlyGenerateMultipleVersionsOfCRDs() { CRDGenerator generator = new CRDGenerator(); final String specVersion = "v1beta1"; final CRDGenerationInfo info = generator - .customResourceClasses(Multiple.class, io.fabric8.crd.example.multiple.v2.Multiple.class) - .forCRDVersions(specVersion) - .withOutput(output).detailedGenerate(); + .customResourceClasses(Multiple.class, io.fabric8.crd.example.multiple.v2.Multiple.class) + .forCRDVersions(specVersion) + .withOutput(output).detailedGenerate(); assertEquals(1, info.numberOfGeneratedCRDs()); final Map> details = info.getCRDDetailsPerNameAndVersion(); @@ -185,13 +185,15 @@ void shouldProperlyGenerateMultipleVersionsOfCRDs() { assertTrue(versions.stream().filter(v -> v.getName().equals("v1")).count() == 1); assertTrue(versions.stream().filter(v -> v.getName().equals("v2")).count() == 1); - - Class[] mustContainTraversedClasses = {Multiple.class, MultipleSpec.class, io.fabric8.crd.example.multiple.v2.Multiple.class, io.fabric8.crd.example.multiple.v2.MultipleSpec.class}; + 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)); + Arrays.stream(mustContainTraversedClasses).map(Class::getCanonicalName) + .forEach(c -> assertTrue(dependentClassNames.contains(c), "should contain " + c)); } - @Test void notDefiningOutputShouldNotGenerateAnything() { + @Test + void notDefiningOutputShouldNotGenerateAnything() { CRDGenerator generator = new CRDGenerator(); assertEquals(0, generator.generate()); @@ -201,24 +203,25 @@ void shouldProperlyGenerateMultipleVersionsOfCRDs() { assertEquals(0, generator.generate()); } - @Test void generatingACycleShouldFail() { + @Test + void generatingACycleShouldFail() { final CRDGenerator generator = new CRDGenerator() - .customResourceClasses(Cyclic.class) - .forCRDVersions("v1", "v1beta1") - .withOutput(output); + .customResourceClasses(Cyclic.class) + .forCRDVersions("v1", "v1beta1") + .withOutput(output); assertThrows( - IllegalArgumentException.class, - () -> generator.detailedGenerate(), - "An IllegalArgument Exception hasn't been thrown when generating a CRD with cyclic references" - ); + IllegalArgumentException.class, + () -> generator.detailedGenerate(), + "An IllegalArgument Exception hasn't been thrown when generating a CRD with cyclic references"); } - @Test void notGeneratingACycleShouldSucceed() { + @Test + void notGeneratingACycleShouldSucceed() { final CRDGenerator generator = new CRDGenerator() - .customResourceClasses(NoCyclic.class) - .forCRDVersions("v1", "v1beta1") - .withOutput(output); + .customResourceClasses(NoCyclic.class) + .forCRDVersions("v1", "v1beta1") + .withOutput(output); CRDGenerationInfo info = generator.detailedGenerate(); assertEquals(2, info.numberOfGeneratedCRDs()); @@ -239,11 +242,12 @@ private void outputCRDIfFailed(Class> customResou throw e; } } + @Test void simplestCRDShouldWork() { outputCRDIfFailed(Simplest.class, (customResource) -> { final CustomResourceDefinitionVersion version = checkCRD(customResource, "Simplest", "simplests", - Scope.CLUSTER, SimplestSpec.class, SimplestStatus.class); + Scope.CLUSTER, SimplestSpec.class, SimplestStatus.class); assertNotNull(version.getSubresources()); }); @@ -253,10 +257,10 @@ 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, ChildSpec.class, ChildStatus.class, BaseSpec.class, BaseStatus.class); assertNotNull(version.getSubresources()); final Map specProps = version.getSchema().getOpenAPIV3Schema() - .getProperties().get("spec").getProperties(); + .getProperties().get("spec").getProperties(); assertEquals(4, specProps.size()); assertEquals("integer", specProps.get("baseInt").getType()); checkMapProp(specProps, "unsupported", "object"); @@ -269,38 +273,45 @@ void inheritedCRDShouldWork() { void mapPropertyShouldHaveCorrectValueType() { outputCRDIfFailed(ContainingMaps.class, (customResource) -> { final CustomResourceDefinitionVersion version = checkCRD(customResource, "ContainingMaps", "containingmaps", - Scope.CLUSTER, ContainingMapsSpec.class); + Scope.CLUSTER, ContainingMapsSpec.class); assertNotNull(version.getSchema()); final Map specProps = version.getSchema().getOpenAPIV3Schema() - .getProperties().get("spec").getProperties(); + .getProperties().get("spec").getProperties(); - assertEquals(2, specProps.size()); + assertEquals(9, specProps.size()); - checkMapProp(specProps, "test", "array"); - String arrayType = specProps.get("test").getAdditionalProperties().getSchema().getItems().getSchema().getType(); - assertEquals("string", arrayType); + JSONSchemaProps testSchema = checkMapProp(specProps, "test", "array"); + assertEquals("string", testSchema.getItems().getSchema().getType()); - checkMapProp(specProps, "test2", "object"); - JSONSchemaProps valueSchema = specProps.get("test2").getAdditionalProperties().getSchema().getAdditionalProperties().getSchema(); + JSONSchemaProps test2Schema = checkMapProp(specProps, "test2", "object"); + JSONSchemaProps valueSchema = test2Schema.getAdditionalProperties().getSchema(); String valueType = valueSchema.getType(); assertEquals("array", valueType); - assertEquals("boolean", valueSchema.getItems().getSchema().getType()); + + for (int i = 1; i <= 7; i++) { + String name = "stringToIntMultiMap" + i; + JSONSchemaProps schema = checkMapProp(specProps, name, "array"); + assertEquals("integer", schema.getItems().getSchema().getType(), name + "'s array item type should be integer"); + } }); } - private void checkMapProp(Map specProps, String name, String valueType) { + private JSONSchemaProps checkMapProp(Map specProps, String name, String valueType) { final JSONSchemaProps props = specProps.get(name); - assertEquals("object", props.getType()); - assertEquals(valueType, props.getAdditionalProperties().getSchema().getType()); + assertNotNull(props, name + " should be contained in spec"); + assertEquals("object", props.getType(), name + "'s type should be object"); + assertEquals(valueType, props.getAdditionalProperties().getSchema().getType(), + name + "'s value type should be " + valueType); + return props.getAdditionalProperties().getSchema(); } @Test void jokeCRDShouldWork() { outputCRDIfFailed(Joke.class, (customResource) -> { CustomResourceDefinitionVersion version = checkCRD(Joke.class, "Joke", "jokes", - Scope.NAMESPACED); + Scope.NAMESPACED); assertNull(version.getSubresources()); }); } @@ -309,10 +320,11 @@ void jokeCRDShouldWork() { 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); + JokeRequestSpec.class, JokeRequestStatus.class, JokeRequestSpec.Category.class, JokeRequestSpec.ExcludedTopic.class, + JokeRequestStatus.State.class); final CustomResourceDefinitionNames names = checkNames("JokeRequest", - "jokerequests", spec); + "jokerequests", spec); assertEquals(1, names.getShortNames().size()); assertTrue(names.getShortNames().contains("jr")); @@ -320,7 +332,7 @@ void jokerequestCRDShouldWork() { assertNotNull(version.getSubresources()); // printer columns should be ordered in the alphabetical order of their json path final List printerColumns = version - .getAdditionalPrinterColumns(); + .getAdditionalPrinterColumns(); assertEquals(2, printerColumns.size()); CustomResourceColumnDefinition columnDefinition = printerColumns.get(0); assertEquals("string", columnDefinition.getType()); @@ -352,7 +364,7 @@ void jokerequestCRDShouldWork() { void checkCRDGenerator() { outputCRDIfFailed(Basic.class, (customResource) -> { final CustomResourceDefinitionVersion version = checkCRD(customResource, "Basic", "basics", - Scope.NAMESPACED, BasicSpec.class, BasicStatus.class); + Scope.NAMESPACED, BasicSpec.class, BasicStatus.class); assertNotNull(version.getSubresources()); CustomResourceValidation schema = version.getSchema(); assertNotNull(schema); @@ -365,8 +377,9 @@ void checkCRDGenerator() { }); } - private CustomResourceDefinitionVersion checkCRD(Class> customResource, String kind, String plural, - Scope scope, Class... traversedClasses) { + private CustomResourceDefinitionVersion checkCRD(Class> customResource, String kind, + String plural, + Scope scope, Class... traversedClasses) { CustomResourceDefinitionSpec spec = checkSpec(customResource, scope, traversedClasses); checkNames(kind, plural, spec); @@ -388,7 +401,7 @@ private CustomResourceDefinitionNames checkNames(String kind, String plural, Cus } private CustomResourceDefinitionSpec checkSpec( - Class> customResource, Scope scope, Class... mustContainTraversedClasses) { + Class> customResource, Scope scope, Class... mustContainTraversedClasses) { CRDGenerator generator = new CRDGenerator(); // record info to be able to output it if the test fails @@ -397,9 +410,9 @@ private CustomResourceDefinitionSpec checkSpec( output.put(outputName, info); final String v1 = "v1"; final CRDGenerationInfo generatedInfo = generator.withOutput(output) - .forCRDVersions(v1) - .customResources(info) - .detailedGenerate(); + .forCRDVersions(v1) + .customResources(info) + .detailedGenerate(); assertEquals(1, generatedInfo.numberOfGeneratedCRDs()); final String crdName = info.crdName(); final Map crdInfos = generatedInfo.getCRDInfos(crdName); @@ -408,9 +421,10 @@ 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) { + 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)); + Arrays.stream(mustContainTraversedClasses).map(Class::getCanonicalName) + .forEach(c -> assertTrue(dependentClassNames.contains(c), "should contain " + c)); } CustomResourceDefinition definition = output.definition(outputName); @@ -431,6 +445,7 @@ private static class TestCRDOutput extends AbstractCRDOutput crdClass = CustomResourceDefinition.class; private final Map infos = new ConcurrentHashMap<>(); + @Override protected ByteArrayOutputStream createStreamFor(String crdName) throws IOException { return new ByteArrayOutputStream(); diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMaps.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMaps.java new file mode 100644 index 0000000000..a0e931a47c --- /dev/null +++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMaps.java @@ -0,0 +1,26 @@ +/** + * 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.maps; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("map.fabric8.io") +@Version("v1") +public class ContainingMaps extends CustomResource { + +} diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMapsCRDTest.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMapsCRDTest.java new file mode 100644 index 0000000000..9b6c4b54bf --- /dev/null +++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMapsCRDTest.java @@ -0,0 +1,64 @@ +/** + * 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.maps; + +import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; +import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionVersion; +import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; +import io.fabric8.kubernetes.client.utils.Serialization; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class ContainingMapsCRDTest { + + @Test + void testCrd() { + CustomResourceDefinition d = Serialization.unmarshal(getClass().getClassLoader() + .getResourceAsStream("META-INF/fabric8/containingmaps.map.fabric8.io-v1.yml"), + CustomResourceDefinition.class); + assertNotNull(d); + + CustomResourceDefinitionVersion v1 = d.getSpec().getVersions().get(0); + assertNotNull(v1); + assertEquals("v1", v1.getName()); + Map spec = v1.getSchema().getOpenAPIV3Schema().getProperties().get("spec").getProperties(); + assertNotNull(spec); + + final Map specProps = v1.getSchema().getOpenAPIV3Schema() + .getProperties().get("spec").getProperties(); + + assertEquals(7, specProps.size()); + + for (int i = 1; i <= 7; i++) { + String name = "stringToIntMultiMap" + i; + JSONSchemaProps schema = checkMapProp(specProps, name, "array"); + assertEquals("integer", schema.getItems().getSchema().getType(), name + "'s array item type should be integer"); + } + } + + private JSONSchemaProps checkMapProp(Map specProps, String name, String valueType) { + final JSONSchemaProps props = specProps.get(name); + assertNotNull(props, name + " should be contained in spec"); + assertEquals("object", props.getType(), name + "'s type should be object"); + assertEquals(valueType, props.getAdditionalProperties().getSchema().getType(), + name + "'s value type should be " + valueType); + return props.getAdditionalProperties().getSchema(); + } +} diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMapsSpec.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMapsSpec.java new file mode 100644 index 0000000000..d1fb7afa7f --- /dev/null +++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/maps/ContainingMapsSpec.java @@ -0,0 +1,51 @@ +/** + * 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.maps; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ContainingMapsSpec { + private MultiHashMap stringToIntMultiMap1; + private MultiMap stringToIntMultiMap2; + private SwappedParametersMap, String> stringToIntMultiMap3; + private RedundantParametersMap> stringToIntMultiMap4; + private RedundantParametersStringToIntMultiMap stringToIntMultiMap5; + private StringKeyedMultiHashMap stringToIntMultiMap6; + private IntValuedMultiMap stringToIntMultiMap7; + + static class MultiHashMap extends HashMap> { + } + + interface MultiMap extends Map> { + } + + interface SwappedParametersMap extends Map { + } + + interface RedundantParametersMap extends Map { + } + + interface RedundantParametersStringToIntMultiMap extends Map> { + } + + static class StringKeyedMultiHashMap extends MultiHashMap { + } + + interface IntValuedMultiMap extends MultiMap { + } +} diff --git a/pom.xml b/pom.xml index 04d2ec13a4..a83a356c45 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,7 @@ UTF-8 - 0.93.0 + 0.93.1 3.12.12 3.12.1_1 1.15.0