From 026659f32f802d680d254c15e4d7d36cc21af060 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 13 Sep 2021 17:10:31 +0200 Subject: [PATCH] refactor: attempt to generate CSVs after k8s manifests are created This is a sync commit. The idea is that we need to be able to read any existing roles or deployments to add that information to the generated CSV. However, so far, either the CSV generation occurs before the manifests are generated, doesn't occur at all or introduces a build item cycle (which is the case with this specific version of the code). --- .../operatorsdk/deployment/CRDGeneration.java | 56 ------- .../operatorsdk/deployment/CSVGenerator.java | 156 ++++++++++++++++++ .../ConfigurationServiceBuildItem.java | 10 +- .../deployment/GeneratedCRDInfoBuildItem.java | 16 ++ .../deployment/GeneratedCSVBuildItem.java | 6 + .../deployment/OperatorSDKProcessor.java | 21 ++- .../runtime/CRDGenerationInfo.java | 11 ++ .../runtime/CustomResourceInfo.java | 24 +++ 8 files changed, 232 insertions(+), 68 deletions(-) create mode 100644 deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/CSVGenerator.java create mode 100644 deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/GeneratedCRDInfoBuildItem.java create mode 100644 deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/GeneratedCSVBuildItem.java diff --git a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/CRDGeneration.java b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/CRDGeneration.java index 721e763d..b3743941 100644 --- a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/CRDGeneration.java +++ b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/CRDGeneration.java @@ -1,22 +1,12 @@ package io.quarkiverse.operatorsdk.deployment; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; - import io.fabric8.crd.generator.CRDGenerator; import io.fabric8.crd.generator.CustomResourceInfo; import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.openshift.api.model.operatorhub.v1alpha1.ClusterServiceVersionBuilder; import io.quarkiverse.operatorsdk.runtime.CRDConfiguration; import io.quarkiverse.operatorsdk.runtime.CRDGenerationInfo; import io.quarkiverse.operatorsdk.runtime.CRDInfo; @@ -28,16 +18,6 @@ class CRDGeneration { private boolean needGeneration; private final CustomResourceControllerMapping crMappings = new CustomResourceControllerMapping(); - private static final ObjectMapper YAML_MAPPER; - static { - YAML_MAPPER = new ObjectMapper((new YAMLFactory()).enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) - .enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS) - .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)); - YAML_MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true); - YAML_MAPPER.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); - YAML_MAPPER.configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false); - } - public CRDGeneration(boolean generate) { this.generate = generate; } @@ -71,24 +51,11 @@ CRDGenerationInfo generate(OutputTargetBuildItem outputTarget, CRDConfiguration final var info = generator.forCRDVersions(crdConfig.versions).inOutputDir(outputDir).detailedGenerate(); final var crdDetailsPerNameAndVersion = info.getCRDDetailsPerNameAndVersion(); - final var controllerToCSVBuilders = new HashMap(7); - crdDetailsPerNameAndVersion.forEach((crdName, initialVersionToCRDInfoMap) -> { OperatorSDKProcessor.log.infov("Generated {0} CRD:", crdName); generated.add(crdName); final var versions = crMappings.getCustomResourceInfos(crdName); - versions.forEach((version, cri) -> controllerToCSVBuilders - .computeIfAbsent(cri.getControllerName(), s -> new ClusterServiceVersionBuilder() - .withNewMetadata().withName(s).endMetadata()) - .editOrNewSpec() - .editOrNewCustomresourcedefinitions() - .addNewOwned() - .withName(crdName) - .withVersion(version) - .withKind(cri.getKind()) - .endOwned().endCustomresourcedefinitions().endSpec()); - final var versionToCRDInfo = converted.computeIfAbsent(crdName, s -> new HashMap<>()); initialVersionToCRDInfoMap .forEach((version, crdInfo) -> { @@ -98,29 +65,6 @@ CRDGenerationInfo generate(OutputTargetBuildItem outputTarget, CRDConfiguration version, filePath, crdInfo.getDependentClassNames(), versions)); }); }); - - controllerToCSVBuilders.forEach((controllerName, csvBuilder) -> { - final File file = new File(outputDir, controllerName + ".csv.yml"); - - // deal with icon - try (var iconAsStream = Thread.currentThread().getContextClassLoader() - .getResourceAsStream(controllerName + ".icon.png"); - var outputStream = new FileOutputStream(file)) { - if (iconAsStream != null) { - final byte[] iconAsBase64 = Base64.getEncoder().encode(iconAsStream.readAllBytes()); - csvBuilder.editOrNewSpec().addNewIcon() - .withBase64data(new String(iconAsBase64)) - .withMediatype("image/png") - .endIcon().endSpec(); - } - - final var csv = csvBuilder.build(); - YAML_MAPPER.writeValue(outputStream, csv); - OperatorSDKProcessor.log.infov("Generated CSV for {0} controller -> {1}", controllerName, file); - } catch (IOException e) { - e.printStackTrace(); - } - }); } return new CRDGenerationInfo(crdConfig.apply, validateCustomResources, converted, generated); } diff --git a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/CSVGenerator.java b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/CSVGenerator.java new file mode 100644 index 00000000..925c9151 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/CSVGenerator.java @@ -0,0 +1,156 @@ +package io.quarkiverse.operatorsdk.deployment; + +import static io.quarkus.kubernetes.deployment.Constants.KUBERNETES; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; + +import io.dekorate.utils.Serialization; +import io.fabric8.kubernetes.api.model.KubernetesList; +import io.fabric8.kubernetes.api.model.ServiceAccount; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.openshift.api.model.ClusterRole; +import io.fabric8.openshift.api.model.PolicyRule; +import io.fabric8.openshift.api.model.Role; +import io.fabric8.openshift.api.model.operatorhub.v1alpha1.ClusterServiceVersionBuilder; +import io.quarkiverse.operatorsdk.runtime.CRDGenerationInfo; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; + +public class CSVGenerator { + private static final ObjectMapper YAML_MAPPER; + + static { + YAML_MAPPER = new ObjectMapper((new YAMLFactory()).enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) + .enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS) + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)); + YAML_MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true); + YAML_MAPPER.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); + YAML_MAPPER.configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false); + } + + public static void generate(OutputTargetBuildItem outputTarget, CRDGenerationInfo info) { + // load generated manifests + final var outputDir = outputTarget.getOutputDirectory().resolve(KUBERNETES); + File manifest = outputDir.resolve("kubernetes.yml").toFile(); + + final var serviceAccountName = new String[1]; + final var clusterRole = new ClusterRole[1]; + final var role = new Role[1]; + final var deployment = new Deployment[1]; + if (manifest.exists()) { + KubernetesList manifests; + try (FileInputStream fis = new FileInputStream(manifest)) { + manifests = Serialization.unmarshalAsList(fis); + } catch (IOException e) { + throw new RuntimeException(e); + } + + manifests.getItems().forEach(i -> { + if (i instanceof ServiceAccount) { + serviceAccountName[0] = i.getMetadata().getName(); + return; + } + + if (i instanceof ClusterRole) { + clusterRole[0] = (ClusterRole) i; + return; + } + + if (i instanceof Role) { + role[0] = (Role) i; + return; + } + + if (i instanceof Deployment) { + deployment[0] = (Deployment) i; + return; + } + }); + } + + final var controllerToCSVBuilders = new HashMap(7); + info.getCrds().forEach((crdName, crdVersionToInfo) -> { + final var versions = info.getCRInfosFor(crdName); + versions.forEach((version, cri) -> controllerToCSVBuilders + .computeIfAbsent(cri.getControllerName(), s -> new ClusterServiceVersionBuilder() + .withNewMetadata().withName(s).endMetadata()) + .editOrNewSpec() + .editOrNewCustomresourcedefinitions() + .addNewOwned() + .withName(crdName) + .withVersion(version) + .withKind(cri.getKind()) + .endOwned().endCustomresourcedefinitions().endSpec()); + }); + + controllerToCSVBuilders.forEach((controllerName, csvBuilder) -> { + final File file = new File(outputDir.toFile(), controllerName + ".csv.yml"); + + final var csvSpec = csvBuilder.editOrNewSpec(); + // deal with icon + try (var iconAsStream = Thread.currentThread().getContextClassLoader() + .getResourceAsStream(controllerName + ".icon.png"); + var outputStream = new FileOutputStream(file)) { + if (iconAsStream != null) { + final byte[] iconAsBase64 = Base64.getEncoder().encode(iconAsStream.readAllBytes()); + csvSpec.addNewIcon() + .withBase64data(new String(iconAsBase64)) + .withMediatype("image/png") + .endIcon(); + } + + final var installSpec = csvSpec.editOrNewInstall() + .editOrNewSpec(); + if (clusterRole[0] != null) { + installSpec + .addNewClusterPermission() + .withServiceAccountName(serviceAccountName[0]) + .addAllToRules(clusterRole[0].getRules().stream().map(CSVGenerator::convertPolicyRule) + .collect(Collectors.toSet())) + .endClusterPermission(); + } + + if (role[0] != null) { + installSpec + .addNewPermission() + .withServiceAccountName(serviceAccountName[0]) + .addAllToRules( + role[0].getRules().stream().map(CSVGenerator::convertPolicyRule) + .collect(Collectors.toSet())) + .endPermission(); + } + + if (deployment[0] != null) { + installSpec.addNewDeployment() + .withName(deployment[0].getMetadata().getName()) + .withSpec(deployment[0].getSpec()) + .endDeployment(); + } + + installSpec.endSpec().endInstall(); + + final var csv = csvBuilder.build(); + YAML_MAPPER.writeValue(outputStream, csv); + OperatorSDKProcessor.log.infov("Generated CSV for {0} controller -> {1}", controllerName, file); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + private static io.fabric8.kubernetes.api.model.rbac.PolicyRule convertPolicyRule(PolicyRule pr) { + return new io.fabric8.kubernetes.api.model.rbac.PolicyRule(pr.getApiGroups(), pr.getNonResourceURLs(), + pr.getResourceNames(), pr.getResources(), pr.getVerbs()); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/ConfigurationServiceBuildItem.java b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/ConfigurationServiceBuildItem.java index ff25ef2f..e1456d98 100644 --- a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/ConfigurationServiceBuildItem.java +++ b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/ConfigurationServiceBuildItem.java @@ -2,7 +2,6 @@ import java.util.List; -import io.quarkiverse.operatorsdk.runtime.CRDGenerationInfo; import io.quarkiverse.operatorsdk.runtime.QuarkusControllerConfiguration; import io.quarkiverse.operatorsdk.runtime.Version; import io.quarkus.builder.item.SimpleBuildItem; @@ -11,13 +10,10 @@ public final class ConfigurationServiceBuildItem extends SimpleBuildItem { private final Version version; private final List controllerConfigs; - private final CRDGenerationInfo crdInfo; - public ConfigurationServiceBuildItem(Version version, - List controllerConfigs, CRDGenerationInfo crdInfo) { + public ConfigurationServiceBuildItem(Version version, List controllerConfigs) { this.version = version; this.controllerConfigs = controllerConfigs; - this.crdInfo = crdInfo; } public Version getVersion() { @@ -27,8 +23,4 @@ public Version getVersion() { public List getControllerConfigs() { return controllerConfigs; } - - public CRDGenerationInfo getCRDGenerationInfo() { - return crdInfo; - } } diff --git a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/GeneratedCRDInfoBuildItem.java b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/GeneratedCRDInfoBuildItem.java new file mode 100644 index 00000000..1fc86279 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/GeneratedCRDInfoBuildItem.java @@ -0,0 +1,16 @@ +package io.quarkiverse.operatorsdk.deployment; + +import io.quarkiverse.operatorsdk.runtime.CRDGenerationInfo; +import io.quarkus.builder.item.SimpleBuildItem; + +public final class GeneratedCRDInfoBuildItem extends SimpleBuildItem { + private final CRDGenerationInfo crdInfo; + + public GeneratedCRDInfoBuildItem(CRDGenerationInfo crdInfo) { + this.crdInfo = crdInfo; + } + + public CRDGenerationInfo getCRDGenerationInfo() { + return crdInfo; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/GeneratedCSVBuildItem.java b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/GeneratedCSVBuildItem.java new file mode 100644 index 00000000..0bfb820d --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/GeneratedCSVBuildItem.java @@ -0,0 +1,6 @@ +package io.quarkiverse.operatorsdk.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class GeneratedCSVBuildItem extends SimpleBuildItem { +} diff --git a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/OperatorSDKProcessor.java b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/OperatorSDKProcessor.java index 5acbe937..19363de9 100644 --- a/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/OperatorSDKProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/OperatorSDKProcessor.java @@ -47,6 +47,7 @@ import io.quarkus.deployment.builditem.nativeimage.ForceNonWeakReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; +import io.quarkus.deployment.pkg.builditem.DeploymentResultBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.util.JandexUtil; import io.quarkus.gizmo.AssignableResultHandle; @@ -97,11 +98,13 @@ void updateControllerConfigurations( ConfigurationServiceRecorder recorder, RunTimeOperatorConfiguration runTimeConfiguration, BuildProducer syntheticBeanBuildItemBuildProducer, - ConfigurationServiceBuildItem serviceBuildItem) { + GeneratedCRDInfoBuildItem generatedCRDs, + ConfigurationServiceBuildItem serviceBuildItem, + GeneratedCSVBuildItem ignored) { final var supplier = recorder .configurationServiceSupplier(serviceBuildItem.getVersion(), serviceBuildItem.getControllerConfigs(), - serviceBuildItem.getCRDGenerationInfo(), + generatedCRDs.getCRDGenerationInfo(), runTimeConfiguration); syntheticBeanBuildItemBuildProducer.produce( SyntheticBeanBuildItem.configure(QuarkusConfigurationService.class) @@ -112,6 +115,15 @@ void updateControllerConfigurations( .done()); } + @BuildStep + void generateCSV(OutputTargetBuildItem outputTarget, GeneratedCRDInfoBuildItem generatedCRDs, + BuildProducer ignored, + DeploymentResultBuildItem sync + // needed to ensure that this step runs after the container image has been built + /* @SuppressWarnings("unused") List artifactResults */) { + CSVGenerator.generate(outputTarget, generatedCRDs.getCRDGenerationInfo()); + } + @BuildStep ConfigurationServiceBuildItem createConfigurationServiceAndOperator( OutputTargetBuildItem outputTarget, @@ -119,6 +131,7 @@ ConfigurationServiceBuildItem createConfigurationServiceAndOperator( BuildProducer additionalBeans, BuildProducer reflectionClasses, BuildProducer forcedReflectionClasses, + BuildProducer generatedCRDInfo, LiveReloadBuildItem liveReload) { final CRDConfiguration crdConfig = buildTimeConfiguration.crd; @@ -158,7 +171,9 @@ ConfigurationServiceBuildItem createConfigurationServiceAndOperator( .build()); } - return new ConfigurationServiceBuildItem(Version.loadFromProperties(), controllerConfigs, crdInfo); + generatedCRDInfo.produce(new GeneratedCRDInfoBuildItem(crdInfo)); + + return new ConfigurationServiceBuildItem(Version.loadFromProperties(), controllerConfigs); } private boolean keep(ClassInfo ci) { diff --git a/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/CRDGenerationInfo.java b/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/CRDGenerationInfo.java index bbb8d641..73c02f99 100644 --- a/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/CRDGenerationInfo.java +++ b/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/CRDGenerationInfo.java @@ -44,4 +44,15 @@ public Map getCRDInfosFor(String crdName) { public boolean isValidateCRDs() { return validateCRDs; } + + public Map getCRInfosFor(String crdName) { + final var crdVersionToInfo = crds.get(crdName); + if (crdVersionToInfo == null) { + throw new IllegalStateException("Should have information associated with '" + crdName + "'"); + } + + Map crVersionToCRInfo = new HashMap<>(7); + crdVersionToInfo.forEach((crdVersion, cri) -> crVersionToCRInfo.putAll(cri.getVersions())); + return crVersionToCRInfo; + } } diff --git a/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/CustomResourceInfo.java b/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/CustomResourceInfo.java index 1bda7a22..6aa75912 100644 --- a/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/CustomResourceInfo.java +++ b/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/CustomResourceInfo.java @@ -90,4 +90,28 @@ public Optional getStatusClassName() { public String getControllerName() { return controllerName; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + CustomResourceInfo that = (CustomResourceInfo) o; + + if (!group.equals(that.group)) + return false; + if (!version.equals(that.version)) + return false; + return kind.equals(that.kind); + } + + @Override + public int hashCode() { + int result = group.hashCode(); + result = 31 * result + version.hashCode(); + result = 31 * result + kind.hashCode(); + return result; + } }