Skip to content

Commit

Permalink
refactor: attempt to generate CSVs after k8s manifests are created
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
metacosm committed Oct 5, 2021
1 parent 19e48a1 commit 026659f
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 68 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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<String, ClusterServiceVersionBuilder>(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) -> {
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, ClusterServiceVersionBuilder>(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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,13 +10,10 @@ public final class ConfigurationServiceBuildItem extends SimpleBuildItem {

private final Version version;
private final List<QuarkusControllerConfiguration> controllerConfigs;
private final CRDGenerationInfo crdInfo;

public ConfigurationServiceBuildItem(Version version,
List<QuarkusControllerConfiguration> controllerConfigs, CRDGenerationInfo crdInfo) {
public ConfigurationServiceBuildItem(Version version, List<QuarkusControllerConfiguration> controllerConfigs) {
this.version = version;
this.controllerConfigs = controllerConfigs;
this.crdInfo = crdInfo;
}

public Version getVersion() {
Expand All @@ -27,8 +23,4 @@ public Version getVersion() {
public List<QuarkusControllerConfiguration> getControllerConfigs() {
return controllerConfigs;
}

public CRDGenerationInfo getCRDGenerationInfo() {
return crdInfo;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.quarkiverse.operatorsdk.deployment;

import io.quarkus.builder.item.SimpleBuildItem;

public final class GeneratedCSVBuildItem extends SimpleBuildItem {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -97,11 +98,13 @@ void updateControllerConfigurations(
ConfigurationServiceRecorder recorder,
RunTimeOperatorConfiguration runTimeConfiguration,
BuildProducer<SyntheticBeanBuildItem> 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)
Expand All @@ -112,13 +115,23 @@ void updateControllerConfigurations(
.done());
}

@BuildStep
void generateCSV(OutputTargetBuildItem outputTarget, GeneratedCRDInfoBuildItem generatedCRDs,
BuildProducer<GeneratedCSVBuildItem> ignored,
DeploymentResultBuildItem sync
// needed to ensure that this step runs after the container image has been built
/* @SuppressWarnings("unused") List<ArtifactResultBuildItem> artifactResults */) {
CSVGenerator.generate(outputTarget, generatedCRDs.getCRDGenerationInfo());
}

@BuildStep
ConfigurationServiceBuildItem createConfigurationServiceAndOperator(
OutputTargetBuildItem outputTarget,
CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<ReflectiveClassBuildItem> reflectionClasses,
BuildProducer<ForceNonWeakReflectiveClassBuildItem> forcedReflectionClasses,
BuildProducer<GeneratedCRDInfoBuildItem> generatedCRDInfo,
LiveReloadBuildItem liveReload) {

final CRDConfiguration crdConfig = buildTimeConfiguration.crd;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,15 @@ public Map<String, CRDInfo> getCRDInfosFor(String crdName) {
public boolean isValidateCRDs() {
return validateCRDs;
}

public Map<String, CustomResourceInfo> getCRInfosFor(String crdName) {
final var crdVersionToInfo = crds.get(crdName);
if (crdVersionToInfo == null) {
throw new IllegalStateException("Should have information associated with '" + crdName + "'");
}

Map<String, CustomResourceInfo> crVersionToCRInfo = new HashMap<>(7);
crdVersionToInfo.forEach((crdVersion, cri) -> crVersionToCRInfo.putAll(cri.getVersions()));
return crVersionToCRInfo;
}
}
Loading

0 comments on commit 026659f

Please sign in to comment.