diff --git a/CHANGELOG.md b/CHANGELOG.md index 4611fd5db5b..76644869673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ #### Improvements +* Fix #6763: CRDGenerator: YAML output customization + #### Dependency Upgrade #### New Features diff --git a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/CRDGenerator.java b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/CRDGenerator.java index 47a7f741f3d..d791efbfa1d 100644 --- a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/CRDGenerator.java +++ b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/CRDGenerator.java @@ -54,6 +54,7 @@ public class CRDGenerator { private ObjectMapper objectMapper; private KubernetesSerialization kubernetesSerialization; private Map infos; + private boolean minQuotes = false; public CRDGenerator inOutputDir(File outputDir) { output = new DirCRDOutput(outputDir); @@ -70,6 +71,11 @@ public CRDGenerator withImplicitPreserveUnknownFields(boolean implicitPreserveUn return this; } + public CRDGenerator withMinQuotes(boolean minQuotes) { + this.minQuotes = minQuotes; + return this; + } + public CRDGenerator withParallelGenerationEnabled(boolean parallel) { this.parallel = parallel; return this; @@ -212,7 +218,7 @@ public void emitCrd(HasMetadata crd, Set dependentClassNames, CRDGenerat final String outputName = getOutputName(crdName, version); try (final OutputStreamWriter writer = new OutputStreamWriter(output.outputFor(outputName), StandardCharsets.UTF_8)) { writer.write("# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!\n"); - String yaml = kubernetesSerialization.asYaml(crd); + String yaml = kubernetesSerialization.asYaml(crd, minQuotes); // strip the explicit start added by default writer.write(yaml.substring(4)); final URI fileURI = output.crdURI(outputName); @@ -284,4 +290,5 @@ public URI crdURI(String crdName) { return getCRDFile(crdName).toURI(); } } + } diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/CRDGeneratorTest.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/CRDGeneratorTest.java index 7873234759d..849ad21446d 100644 --- a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/CRDGeneratorTest.java +++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/CRDGeneratorTest.java @@ -60,6 +60,7 @@ import static io.fabric8.crdv2.generator.CRDGeneratorAssertions.assertFileEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -103,6 +104,7 @@ void choosingCRDVersionsShouldWork() { @Test void addingCustomResourceInfosShouldWork() { CRDGenerator generator = newCRDGenerator(); + assertTrue(generator.getCustomResourceInfos().isEmpty()); generator.customResourceClasses(); @@ -477,6 +479,66 @@ void checkGenerationIsDeterministic() throws Exception { assertTrue(outputDir.delete()); } + @Test + void checkMinQuotesDefault() throws Exception { + final File outputDir = Files.createTempDirectory("crd-").toFile(); + final String crdName = CustomResourceInfo.fromClass(Complex.class).crdName(); + CRDGenerationInfo crdInfo = newCRDGenerator() + .inOutputDir(outputDir) + .forCRDVersions("v1") + .customResourceClasses(Complex.class) + .detailedGenerate(); + + File crdFile = new File(crdInfo.getCRDInfos(crdName).get("v1").getFilePath()); + String crd = Files.readString(crdFile.toPath()); + assertTrue(crd.contains("\"complexkinds.example.com\"")); + + // only delete the generated files if the test is successful + assertTrue(crdFile.delete()); + assertTrue(outputDir.delete()); + } + + @Test + void checkMinQuotesFalse() throws Exception { + final File outputDir = Files.createTempDirectory("crd-").toFile(); + final String crdName = CustomResourceInfo.fromClass(Complex.class).crdName(); + CRDGenerationInfo crdInfo = newCRDGenerator() + .inOutputDir(outputDir) + .forCRDVersions("v1") + .customResourceClasses(Complex.class) + .withMinQuotes(false) + .detailedGenerate(); + + File crdFile = new File(crdInfo.getCRDInfos(crdName).get("v1").getFilePath()); + String crd = Files.readString(crdFile.toPath()); + assertTrue(crd.contains("\"complexkinds.example.com\"")); + + // only delete the generated files if the test is successful + assertTrue(crdFile.delete()); + assertTrue(outputDir.delete()); + } + + @Test + void checkMinQuotesTrue() throws Exception { + final File outputDir = Files.createTempDirectory("crd-").toFile(); + final String crdName = CustomResourceInfo.fromClass(Complex.class).crdName(); + CRDGenerationInfo crdInfo = newCRDGenerator() + .inOutputDir(outputDir) + .forCRDVersions("v1") + .customResourceClasses(Complex.class) + .withMinQuotes(true) + .detailedGenerate(); + + File crdFile = new File(crdInfo.getCRDInfos(crdName).get("v1").getFilePath()); + String crd = Files.readString(crdFile.toPath()); + assertTrue(crd.contains("complexkinds.example.com")); + assertFalse(crd.contains("\"complexkinds.example.com\"")); + + // only delete the generated files if the test is successful + assertTrue(crdFile.delete()); + assertTrue(outputDir.delete()); + } + @RepeatedTest(value = 10) void checkGenerationMultipleVersionsOfCRDsIsDeterministic() throws Exception { // generated CRD diff --git a/crd-generator/maven-plugin/src/main/java/io/fabric8/crd/generator/maven/plugin/CrdGeneratorMojo.java b/crd-generator/maven-plugin/src/main/java/io/fabric8/crd/generator/maven/plugin/CrdGeneratorMojo.java index 3995d033c30..bef3b6e3727 100644 --- a/crd-generator/maven-plugin/src/main/java/io/fabric8/crd/generator/maven/plugin/CrdGeneratorMojo.java +++ b/crd-generator/maven-plugin/src/main/java/io/fabric8/crd/generator/maven/plugin/CrdGeneratorMojo.java @@ -129,6 +129,12 @@ public class CrdGeneratorMojo extends AbstractMojo { @Parameter(property = "fabric8.crd-generator.skip", defaultValue = "false") boolean skip; + /** + * If {@code true}, quotes will only be included where necessary + */ + @Parameter(property = "fabric8.crd-generator.minimizeQuotes", defaultValue = "false") + boolean minimizeQuotes; + private final CustomResourceCollector customResourceCollector; private final CRDGenerator crdGenerator; @@ -178,6 +184,7 @@ public void execute() throws MojoExecutionException { .customResourceClasses(customResourceClassesLoaded) .withParallelGenerationEnabled(parallel) .withImplicitPreserveUnknownFields(implicitPreserveUnknownFields) + .withMinQuotes(minimizeQuotes) .inOutputDir(outputDirectory); CRDGenerationInfo crdGenerationInfo = crdGenerator.detailedGenerate(); 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 7f7bb505178..9e89598d3cb 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 @@ -188,6 +188,23 @@ public String asJson(T object) { * @return a String containing a JSON representation of the provided object. */ public String asYaml(T object) { + return asYaml(object, false); + } + + /** + * Returns a YAML representation of the given object. + * + *

+ * If the provided object contains a JsonAnyGetter annotated method with a Map that contains an entry that + * overrides a field of the provided object, the Map entry will take precedence upon serialization. Properties won't + * be duplicated. + * + * @param object the object to serialize. + * @param minQuotes whether strings will be rendered without quotes (true) or with quotes (false). + * @param the type of the object being serialized. + * @return a String containing a JSON representation of the provided object. + */ + public String asYaml(T object, boolean minQuotes) { DumpSettings settings = DumpSettings.builder() .setExplicitStart(true).setDefaultFlowStyle(FlowStyle.BLOCK).build(); final Dump yaml = new Dump(settings, new StandardRepresenter(settings) { @@ -207,7 +224,7 @@ protected NodeTuple representMappingEntry(java.util.Map.Entry entry) { } } org.snakeyaml.engine.v2.nodes.Node nodeKey = representData(key); - quote = true; + quote = !minQuotes; return new NodeTuple(nodeKey, representData(entry.getValue())); } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesSerializationTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesSerializationTest.java index 57a789ea127..6dc8f26f0ff 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesSerializationTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesSerializationTest.java @@ -22,6 +22,7 @@ import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.CustomResourceDefinition; import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Version; import org.junit.jupiter.api.BeforeEach; @@ -33,6 +34,11 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -65,6 +71,30 @@ void withRegisteredKubernetesResourceShouldDeserializeToPod() { .isInstanceOf(io.fabric8.kubernetes.api.model.Pod.class); } + @Test + void asYaml() throws Exception { + final String input = readYamlToString("/serialization/test-crd-schema.yml"); + final CustomResourceDefinition crd = Serialization.unmarshal(input, CustomResourceDefinition.class); + + String result = kubernetesSerialization.asYaml(crd); + assertThat(result).asString().contains("\"widgets.test.fabric8.io\""); + + result = kubernetesSerialization.asYaml(crd, false); + assertThat(result).asString().contains("\"widgets.test.fabric8.io\""); + + result = kubernetesSerialization.asYaml(crd, true); + assertThat(result).asString().doesNotContain("\"widgets.test.fabric8.io\""); + assertThat(result).asString().contains("widgets.test.fabric8.io"); + } + + private String readYamlToString(String path) throws IOException { + return Files.readAllLines( + new File(KubernetesSerializationTest.class.getResource(path).getFile()).toPath(), StandardCharsets.UTF_8) + .stream() + .filter(line -> !line.startsWith("#")) + .collect(Collectors.joining("\n")); + } + @ParameterizedTest(name = "{index}: {0} {1} deserializes to {2}") @MethodSource("sameGVK") void withCollidingRegisteredKubernetesResourceShouldDeserializeAppropriate(