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; + } }