diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b3dd11398..e0991692fd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ #### New Features +* Fix #6827: Add CRDPostProcessor to process generated CRDs before they are written out + #### _**Note**_: Breaking changes ### 7.1.0 (2025-01-30) 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 9f22f7948d0..3670c01ac68 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 @@ -48,6 +48,8 @@ public class CRDGenerator { private static final Logger LOGGER = LoggerFactory.getLogger(CRDGenerator.class); + private static final CRDPostProcessor nullProcessor = new CRDPostProcessor() { + }; private final Map handlers = new HashMap<>(2); private CRDOutput output; private boolean parallel; @@ -56,10 +58,10 @@ public class CRDGenerator { private KubernetesSerialization kubernetesSerialization; private Map infos; private boolean minQuotes = false; + private CRDPostProcessor postProcessor = nullProcessor; public CRDGenerator inOutputDir(File outputDir) { - output = new DirCRDOutput(outputDir); - return this; + return withOutput(new DirCRDOutput(outputDir)); } public CRDGenerator withOutput(CRDOutput output) { @@ -88,6 +90,11 @@ public CRDGenerator withObjectMapper(ObjectMapper mapper, KubernetesSerializatio return this; } + public CRDGenerator withPostProcessor(CRDPostProcessor postProcessor) { + this.postProcessor = postProcessor; + return this; + } + public CRDGenerator forCRDVersions(List versions) { return versions != null && !versions.isEmpty() ? forCRDVersions(versions.toArray(new String[0])) : this; @@ -115,9 +122,7 @@ Map getHandlers() { return handlers; } - // this is public API, so we cannot change the signature, so there is no way to prevent the possible heap pollution - // (we also cannot use @SafeVarargs, because that requires the method to be final, which is another signature change) - @SuppressWarnings("unchecked") + @SafeVarargs public final CRDGenerator customResourceClasses(Class... crClasses) { if (crClasses == null) { return this; @@ -163,6 +168,12 @@ public int generate() { return detailedGenerate().numberOfGeneratedCRDs(); } + /** + * Generates the CRDs with the provided configuration and returns detailed information about what was generated as a + * {@link CRDGenerationInfo} instance. + * + * @return a {@link CRDGenerationInfo} providing detailed information about what was generated + */ public CRDGenerationInfo detailedGenerate() { if (getCustomResourceInfos().isEmpty()) { LOGGER.warn("No resources were registered with the 'customResources' method to be generated"); @@ -201,9 +212,10 @@ public CRDGenerationInfo detailedGenerate() { if (parallel) { handlers.values().stream().map(h -> ForkJoinPool.commonPool().submit(() -> h.handle(info, context.forkContext()))) - .collect(Collectors.toList()).stream().forEach(ForkJoinTask::join); + .collect(Collectors.toList()) + .forEach(ForkJoinTask::join); } else { - handlers.values().stream().forEach(h -> h.handle(info, context.forkContext())); + handlers.values().forEach(h -> h.handle(info, context.forkContext())); } } } @@ -217,6 +229,10 @@ public CRDGenerationInfo detailedGenerate() { public void emitCrd(HasMetadata crd, Set dependentClassNames, CRDGenerationInfo crdGenerationInfo) { final String version = ApiVersionUtil.trimVersion(crd.getApiVersion()); final String crdName = crd.getMetadata().getName(); + + // post-process the CRD if needed + crd = postProcessor.process(crd, version); + try { final String outputName = getOutputName(crdName, version); try (final OutputStreamWriter writer = new OutputStreamWriter(output.outputFor(outputName), StandardCharsets.UTF_8)) { diff --git a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/CRDPostProcessor.java b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/CRDPostProcessor.java new file mode 100644 index 00000000000..2939ff261fd --- /dev/null +++ b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/CRDPostProcessor.java @@ -0,0 +1,33 @@ +/* + * 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.crdv2.generator; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface CRDPostProcessor { + + /** + * Processes the specified CRD (passed as {@link HasMetadata} to be able to handle multiple versions of the CRD spec) after + * they are generated but before they are written out + * + * @param crd the CRD to process as a {@link HasMetadata} + * @param crdSpecVersion the CRD specification version of the CRD to process (to be able to cast to the appropriate CRD class) + * @return the modified CRD (though, typically, this would just be the CRD specified as input, modified in place) + */ + default HasMetadata process(HasMetadata crd, String crdSpecVersion) { + return crd; + } +} 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 849ad21446d..46ccca05d18 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 @@ -38,6 +38,7 @@ import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceValidation; import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.model.Scope; import org.junit.jupiter.api.RepeatedTest; @@ -49,6 +50,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.net.URL; import java.nio.file.Files; import java.util.ArrayList; @@ -595,6 +597,38 @@ void checkK8sValidationRules() throws Exception { assertTrue(outputDir.delete()); } + @Test + void checkPostProcessing() throws Exception { + // generated CRD + final File outputDir = Files.createTempDirectory("crd-").toFile(); + final String crdName = CustomResourceInfo.fromClass(Simplest.class).crdName(); + + final CRDGenerationInfo crdInfo = newCRDGenerator() + .inOutputDir(outputDir) + .customResourceClasses(Simplest.class) + .forCRDVersions("v1") + .withPostProcessor(new CRDPostProcessor() { + @Override + public HasMetadata process(HasMetadata crd, String crdSpecVersion) { + final var meta = crd.getMetadata().edit().addToLabels("foo", "bar").build(); + crd.setMetadata(meta); + return crd; + } + }) + .detailedGenerate(); + + final File crdFile = new File(crdInfo.getCRDInfos(crdName).get("v1").getFilePath()); + final var serialization = new KubernetesSerialization(); + final var crd = serialization.unmarshal(new FileInputStream(crdFile), HasMetadata.class); + assertNotNull(crd); + assertEquals(crdName, crd.getMetadata().getName()); + assertEquals("bar", crd.getMetadata().getLabels().get("foo")); + + // only delete the generated files if the test is successful + assertTrue(crdFile.delete()); + assertTrue(outputDir.delete()); + } + private CustomResourceDefinitionVersion checkCRD(Class> customResource, String kind, String plural, Scope scope) { diff --git a/doc/CRD-generator-migration-v2.md b/doc/CRD-generator-migration-v2.md index b54d63432fc..1e49ae044e7 100644 --- a/doc/CRD-generator-migration-v2.md +++ b/doc/CRD-generator-migration-v2.md @@ -16,7 +16,8 @@ _Deprecated since 7.0.0_ _GA since 7.0.0_ - **CRD Generator API v2** - `io.fabric8:crd-generator-api-v2` - _Core implementation of the new generator, based on [Jackson/jsonSchema](https://github.com/FasterXML/jackson-module-jsonSchema)._ + _Core implementation of the new generator, based + on [Jackson/jsonSchema](https://github.com/FasterXML/jackson-module-jsonSchema)._ - **CRD Generator Collector** - `io.fabric8:crd-generator-collector` _Shared component to find and load compiled Custom Resource classes in directories and Jar files._ - **CRD Generator Maven Plugin** - `io.fabric8:crd-generator-maven-plugin` @@ -101,7 +102,12 @@ myMap: ## Default values for CRD fields can be numeric or boolean -Previously default values defined by `@Default` could only be used on string fields. +Previously default values defined by `@Default` could only be used on string fields. With CRD Generator v2 defaults can be set on numeric and boolean fields, too. In the same way is `@JsonProperty(defaultValue)` now working. +## Post-processing CRDs before they are written out to disk + +It is now possible to provide a `CRDPostProcessor` implementation when generating CRDs via the +`CRDGenerator.detailedGenerate` method. This allows to process generated CRDs before they are written out to the disk. + diff --git a/junit/kube-api-test/core/src/test/java/io/fabric8/kubeapitest/binary/BinaryDownloaderTest.java b/junit/kube-api-test/core/src/test/java/io/fabric8/kubeapitest/binary/BinaryDownloaderTest.java index f167ff30d43..3bbbf3a4f05 100644 --- a/junit/kube-api-test/core/src/test/java/io/fabric8/kubeapitest/binary/BinaryDownloaderTest.java +++ b/junit/kube-api-test/core/src/test/java/io/fabric8/kubeapitest/binary/BinaryDownloaderTest.java @@ -34,19 +34,19 @@ class BinaryDownloaderTest { BinaryDownloader binaryDownloader = new BinaryDownloader("target", mockBinaryRepo, mockOsInfo); @Test - void findsLatestBinary() { - when(mockOsInfo.getOSName()).thenReturn("linux"); - when(mockOsInfo.getOSArch()).thenReturn("amd64"); + void findsLatestBinary() { + when(mockOsInfo.getOSName()).thenReturn("linux"); + when(mockOsInfo.getOSArch()).thenReturn("amd64"); - when(mockBinaryRepo.listObjectNames()).thenReturn(List.of( - new ArchiveDescriptor("1.17.9","amd64","linux"), - new ArchiveDescriptor("1.26.1","amd64","darwin"), - new ArchiveDescriptor(VERSION,"amd64","linux")) - .stream()); + when(mockBinaryRepo.listObjectNames()).thenReturn(List.of( + new ArchiveDescriptor("1.17.9", "amd64", "linux"), + new ArchiveDescriptor("1.26.1", "amd64", "darwin"), + new ArchiveDescriptor(VERSION, "amd64", "linux")) + .stream()); - var latest = binaryDownloader.findLatestVersion(); + var latest = binaryDownloader.findLatestVersion(); - assertThat(latest).isEqualTo(VERSION); - } + assertThat(latest).isEqualTo(VERSION); + } } diff --git a/junit/kube-api-test/core/src/test/java/io/fabric8/kubeapitest/kubeconfig/KubeConfigTest.java b/junit/kube-api-test/core/src/test/java/io/fabric8/kubeapitest/kubeconfig/KubeConfigTest.java index 5541c11c3ba..00ae4353e8f 100644 --- a/junit/kube-api-test/core/src/test/java/io/fabric8/kubeapitest/kubeconfig/KubeConfigTest.java +++ b/junit/kube-api-test/core/src/test/java/io/fabric8/kubeapitest/kubeconfig/KubeConfigTest.java @@ -33,18 +33,18 @@ class KubeConfigTest { KubeConfig kubeConfig = new KubeConfig(certManagerMock, null); @Test - void generatesConfigYaml() { - when(certManagerMock.getAPIServerCertPath()).thenReturn(API_CERT_PATH); - when(certManagerMock.getClientCertPath()).thenReturn(CLIENT_KEY_PATH); - when(certManagerMock.getClientKeyPath()).thenReturn(CLIENT_CERT_PATH); - - String yaml = kubeConfig.generateKubeConfigYaml(API_SERVER_PORT); - - assertThat(yaml) - .contains(""+API_SERVER_PORT) - .contains(API_CERT_PATH) - .contains(CLIENT_CERT_PATH) - .contains(CLIENT_KEY_PATH); - } + void generatesConfigYaml() { + when(certManagerMock.getAPIServerCertPath()).thenReturn(API_CERT_PATH); + when(certManagerMock.getClientCertPath()).thenReturn(CLIENT_KEY_PATH); + when(certManagerMock.getClientKeyPath()).thenReturn(CLIENT_CERT_PATH); + + String yaml = kubeConfig.generateKubeConfigYaml(API_SERVER_PORT); + + assertThat(yaml) + .contains("" + API_SERVER_PORT) + .contains(API_CERT_PATH) + .contains(CLIENT_CERT_PATH) + .contains(CLIENT_KEY_PATH); + } }