Skip to content

Commit

Permalink
feat: support for additional kubernetes resource in helm charts (#769)
Browse files Browse the repository at this point in the history
* fix: add rest of resources to helm resources

Signed-off-by: Attila Mészáros <csviri@gmail.com>

* impl

Signed-off-by: Attila Mészáros <csviri@gmail.com>

* filtering fixes

Signed-off-by: Attila Mészáros <csviri@gmail.com>

* revert test changes

Signed-off-by: Attila Mészáros <csviri@gmail.com>

* refactor: make build step conditional

Signed-off-by: Chris Laprun <claprun@redhat.com>

* refactor: make deserialization of kube resources a separate step

This allows for this to happen only once as needed and could be done in
parallel to other build steps.

Signed-off-by: Chris Laprun <claprun@redhat.com>

---------

Signed-off-by: Attila Mészáros <csviri@gmail.com>
Signed-off-by: Chris Laprun <claprun@redhat.com>
Co-authored-by: Chris Laprun <claprun@redhat.com>
  • Loading branch information
csviri and metacosm authored Dec 12, 2023
1 parent 44bf6aa commit 503735d
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.kubernetes.deployment.KubernetesConfig;
import io.quarkus.kubernetes.deployment.ResourceNameUtil;
import io.quarkus.kubernetes.spi.GeneratedKubernetesResourceBuildItem;

public class BundleProcessor {

Expand Down Expand Up @@ -183,77 +182,74 @@ void generateBundle(ApplicationInfoBuildItem configuration,
VersionBuildItem versionBuildItem,
BuildProducer<GeneratedBundleBuildItem> doneGeneratingCSV,
GeneratedCRDInfoBuildItem generatedCustomResourcesDefinitions,
List<GeneratedKubernetesResourceBuildItem> generatedKubernetesManifests,
DeserializedKubernetesResourcesBuildItem generatedKubernetesResources,
BuildProducer<GeneratedFileSystemResourceBuildItem> generatedCSVs) {
if (bundleConfiguration.enabled) {
final var crds = generatedCustomResourcesDefinitions.getCRDGenerationInfo().getCrds()
.values().stream()
.flatMap(entry -> entry.values().stream())
.collect(Collectors.toMap(CRDInfo::getCrdName, Function.identity()));
final var outputDir = outputTarget.getOutputDirectory().resolve(BUNDLE);
final var serviceAccounts = new LinkedList<ServiceAccount>();
final var clusterRoleBindings = new LinkedList<ClusterRoleBinding>();
final var clusterRoles = new LinkedList<ClusterRole>();
final var roleBindings = new LinkedList<RoleBinding>();
final var roles = new LinkedList<Role>();
final var deployments = new LinkedList<Deployment>();

final var resources = GeneratedResourcesUtils.loadFrom(generatedKubernetesManifests);
resources.forEach(r -> {
if (r instanceof ServiceAccount) {
serviceAccounts.add((ServiceAccount) r);
return;
}
final var crds = generatedCustomResourcesDefinitions.getCRDGenerationInfo().getCrds()
.values().stream()
.flatMap(entry -> entry.values().stream())
.collect(Collectors.toMap(CRDInfo::getCrdName, Function.identity()));
final var outputDir = outputTarget.getOutputDirectory().resolve(BUNDLE);
final var serviceAccounts = new LinkedList<ServiceAccount>();
final var clusterRoleBindings = new LinkedList<ClusterRoleBinding>();
final var clusterRoles = new LinkedList<ClusterRole>();
final var roleBindings = new LinkedList<RoleBinding>();
final var roles = new LinkedList<Role>();
final var deployments = new LinkedList<Deployment>();

final var resources = generatedKubernetesResources.getResources();
resources.forEach(r -> {
if (r instanceof ServiceAccount) {
serviceAccounts.add((ServiceAccount) r);
return;
}

if (r instanceof ClusterRoleBinding) {
clusterRoleBindings.add((ClusterRoleBinding) r);
return;
}
if (r instanceof ClusterRoleBinding) {
clusterRoleBindings.add((ClusterRoleBinding) r);
return;
}

if (r instanceof ClusterRole) {
clusterRoles.add((ClusterRole) r);
return;
}
if (r instanceof ClusterRole) {
clusterRoles.add((ClusterRole) r);
return;
}

if (r instanceof RoleBinding) {
roleBindings.add((RoleBinding) r);
return;
}
if (r instanceof RoleBinding) {
roleBindings.add((RoleBinding) r);
return;
}

if (r instanceof Role) {
roles.add((Role) r);
return;
}
if (r instanceof Role) {
roles.add((Role) r);
return;
}

if (r instanceof Deployment) {
deployments.add((Deployment) r);
}
});

final var deploymentName = ResourceNameUtil.getResourceName(kubernetesConfig, configuration);
final var generated = BundleGenerator.prepareGeneration(bundleConfiguration, operatorConfiguration,
versionBuildItem.getVersion(),
csvMetadata.getCsvGroups(), crds, outputTarget.getOutputDirectory(), deploymentName);
generated.forEach(manifestBuilder -> {
final var fileName = manifestBuilder.getFileName();
try {
generatedCSVs.produce(
new GeneratedFileSystemResourceBuildItem(
Path.of(BUNDLE).resolve(manifestBuilder.getName()).resolve(fileName).toString(),
manifestBuilder.getManifestData(serviceAccounts, clusterRoleBindings, clusterRoles,
roleBindings, roles, deployments)));
log.infov("Generating {0} for ''{1}'' controller -> {2}",
manifestBuilder.getManifestType(),
manifestBuilder.getName(),
outputDir.resolve(manifestBuilder.getName()).resolve(fileName));
} catch (IOException e) {
log.errorv("Cannot generate {0} for ''{1}'' controller: {2}",
manifestBuilder.getManifestType(), manifestBuilder.getName(), e.getMessage());
}
});
doneGeneratingCSV.produce(new GeneratedBundleBuildItem());
if (r instanceof Deployment) {
deployments.add((Deployment) r);
}
});

}
final var deploymentName = ResourceNameUtil.getResourceName(kubernetesConfig, configuration);
final var generated = BundleGenerator.prepareGeneration(bundleConfiguration, operatorConfiguration,
versionBuildItem.getVersion(),
csvMetadata.getCsvGroups(), crds, outputTarget.getOutputDirectory(), deploymentName);
generated.forEach(manifestBuilder -> {
final var fileName = manifestBuilder.getFileName();
try {
generatedCSVs.produce(
new GeneratedFileSystemResourceBuildItem(
Path.of(BUNDLE).resolve(manifestBuilder.getName()).resolve(fileName).toString(),
manifestBuilder.getManifestData(serviceAccounts, clusterRoleBindings, clusterRoles,
roleBindings, roles, deployments)));
log.infov("Generating {0} for ''{1}'' controller -> {2}",
manifestBuilder.getManifestType(),
manifestBuilder.getName(),
outputDir.resolve(manifestBuilder.getName()).resolve(fileName));
} catch (IOException e) {
log.errorv("Cannot generate {0} for ''{1}'' controller: {2}",
manifestBuilder.getManifestType(), manifestBuilder.getName(), e.getMessage());
}
});
doneGeneratingCSV.produce(new GeneratedBundleBuildItem());
}

private Map<String, CSVMetadataHolder> getSharedMetadataHolders(String name, String version, String defaultReplaces,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkiverse.operatorsdk.common;

import java.util.List;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.quarkus.builder.item.SimpleBuildItem;

public final class DeserializedKubernetesResourcesBuildItem extends SimpleBuildItem {
private final List<HasMetadata> resources;

public DeserializedKubernetesResourcesBuildItem(List<HasMetadata> resources) {
this.resources = resources;
}

public List<HasMetadata> getResources() {
return resources;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkiverse.operatorsdk.deployment;

import java.util.List;
import java.util.function.BooleanSupplier;

import org.eclipse.microprofile.config.ConfigProvider;

import io.quarkiverse.operatorsdk.common.DeserializedKubernetesResourcesBuildItem;
import io.quarkiverse.operatorsdk.common.GeneratedResourcesUtils;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.kubernetes.spi.GeneratedKubernetesResourceBuildItem;

public class GeneratedKubernetesManifestsProcessor {
private static class NeedResourcesDeserialization implements BooleanSupplier {
@Override
public boolean getAsBoolean() {
final var helmEnabled = ConfigProvider.getConfig()
.getOptionalValue("quarkus.operator-sdk.helm.enabled", Boolean.class).orElse(false);
final var bundleEnabled = ConfigProvider.getConfig()
.getOptionalValue("quarkus.operator-sdk.bundle.enabled", Boolean.class).orElse(false);
return helmEnabled || bundleEnabled;
}
}

@BuildStep(onlyIf = NeedResourcesDeserialization.class)
DeserializedKubernetesResourcesBuildItem deserializeGeneratedKubernetesResources(
List<GeneratedKubernetesResourceBuildItem> generatedResources) {
return new DeserializedKubernetesResourcesBuildItem(GeneratedResourcesUtils.loadFrom(generatedResources));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;

import org.jboss.logging.Logger;

import io.dekorate.helm.model.Chart;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.rbac.ClusterRole;
import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding;
import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
import io.quarkiverse.operatorsdk.common.DeserializedKubernetesResourcesBuildItem;
import io.quarkiverse.operatorsdk.common.FileUtils;
import io.quarkiverse.operatorsdk.common.GeneratedResourcesUtils;
import io.quarkiverse.operatorsdk.deployment.AddClusterRolesDecorator;
import io.quarkiverse.operatorsdk.deployment.ControllerConfigurationsBuildItem;
import io.quarkiverse.operatorsdk.deployment.GeneratedCRDInfoBuildItem;
Expand All @@ -28,11 +32,11 @@
import io.quarkus.container.spi.ContainerImageInfoBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Produce;
import io.quarkus.deployment.builditem.ApplicationInfoBuildItem;
import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem;
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.kubernetes.spi.ConfiguratorBuildItem;
import io.quarkus.kubernetes.spi.GeneratedKubernetesResourceBuildItem;
import io.quarkus.kubernetes.spi.*;
import io.quarkus.qute.Qute;

public class HelmChartProcessor {
Expand All @@ -56,45 +60,90 @@ public class HelmChartProcessor {
public static final String CRD_DIR = "crds";
public static final String CRD_ROLE_BINDING_TEMPLATE_PATH = "/helm/crd-role-binding-template.yaml";

@BuildStep
public void handleHelmCharts(
// to make it produce a build item, so it gets executed
@SuppressWarnings("unused") BuildProducer<ArtifactResultBuildItem> dummy,
List<GeneratedKubernetesResourceBuildItem> generatedResources,
private static class HelmGenerationEnabled implements BooleanSupplier {
private BuildTimeOperatorConfiguration config;

@Override
public boolean getAsBoolean() {
return config.helm.enabled;
}
}

@BuildStep(onlyIfNot = HelmGenerationEnabled.class)
@Produce(ArtifactResultBuildItem.class)
void outputHelmGenerationDisabled() {
log.debug("Generating Helm chart is disabled");
}

@BuildStep(onlyIf = HelmGenerationEnabled.class)
@Produce(ArtifactResultBuildItem.class) // to make it produce a build item, so it gets executed
void handleHelmCharts(DeserializedKubernetesResourcesBuildItem generatedKubernetesResources,
ControllerConfigurationsBuildItem controllerConfigurations,
BuildTimeOperatorConfiguration buildTimeConfiguration,
GeneratedCRDInfoBuildItem generatedCRDInfoBuildItem,
OutputTargetBuildItem outputTarget,
ApplicationInfoBuildItem appInfo,
ContainerImageInfoBuildItem containerImageInfoBuildItem) {

if (buildTimeConfiguration.helm.enabled) {
final var helmDir = outputTarget.getOutputDirectory().resolve("helm").toFile();
log.infov("Generating helm chart to {0}", helmDir);
var controllerConfigs = controllerConfigurations.getControllerConfigs().values();

createRelatedDirectories(helmDir);
addTemplateFiles(helmDir);
addClusterRolesForReconcilers(helmDir, controllerConfigs);
addPrimaryClusterRoleBindings(helmDir, controllerConfigs);
addGeneratedDeployment(helmDir, generatedResources, controllerConfigurations, appInfo);
addChartYaml(helmDir, appInfo.getName(), appInfo.getVersion());
addValuesYaml(helmDir, containerImageInfoBuildItem.getTag());
addReadmeAndSchema(helmDir);
addCRDs(new File(helmDir, CRD_DIR), generatedCRDInfoBuildItem);
} else {
log.debug("Generating helm chart is disabled");
final var helmDir = outputTarget.getOutputDirectory().resolve("helm").toFile();
log.infov("Generating helm chart to {0}", helmDir);
var controllerConfigs = controllerConfigurations.getControllerConfigs().values();

final var resources = generatedKubernetesResources.getResources();
createRelatedDirectories(helmDir);
addTemplateFiles(helmDir);
addClusterRolesForReconcilers(helmDir, controllerConfigs);
addPrimaryClusterRoleBindings(helmDir, controllerConfigs);
addGeneratedDeployment(helmDir, resources, controllerConfigurations, appInfo);
addChartYaml(helmDir, appInfo.getName(), appInfo.getVersion());
addValuesYaml(helmDir, containerImageInfoBuildItem.getTag());
addReadmeAndSchema(helmDir);
addCRDs(new File(helmDir, CRD_DIR), generatedCRDInfoBuildItem);
addExplicitlyAddedKubernetesResources(helmDir, resources, appInfo);
}

private void addExplicitlyAddedKubernetesResources(File helmDir,
List<HasMetadata> resources, ApplicationInfoBuildItem appInfo) {
resources = filterOutStandardResources(resources, appInfo);
if (!resources.isEmpty()) {
addResourceToHelmDir(helmDir, resources);
}
}

private List<HasMetadata> filterOutStandardResources(List<HasMetadata> resources, ApplicationInfoBuildItem appInfo) {
return resources.stream().filter(r -> {
if (r instanceof ClusterRole) {
return !r.getMetadata().getName().endsWith("-cluster-role");
}
if (r instanceof ClusterRoleBinding) {
return !r.getMetadata().getName().endsWith("-crd-validating-role-binding");
}
if (r instanceof RoleBinding) {
return !r.getMetadata().getName().equals(appInfo.getName() + "-view") &&
!r.getMetadata().getName().endsWith("-role-binding");
}
if (r instanceof Service || r instanceof Deployment || r instanceof ServiceAccount) {
return !r.getMetadata().getName().equals(appInfo.getName());
}
return true;
}).collect(Collectors.toList());
}

private void addResourceToHelmDir(File helmDir, List<HasMetadata> list) {
String yaml = FileUtils.asYaml(list);
try {
Files.writeString(Path.of(helmDir.getPath(), TEMPLATES_DIR, "kubernetes.yml"), yaml);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}

private void addTemplateFiles(File helmDir) {
copyTemplates(helmDir.toPath().resolve(TEMPLATES_DIR), TEMPLATE_FILES);
}

private void addGeneratedDeployment(File helmDir, List<GeneratedKubernetesResourceBuildItem> generatedResources,
private void addGeneratedDeployment(File helmDir, List<HasMetadata> resources,
ControllerConfigurationsBuildItem controllerConfigurations,
ApplicationInfoBuildItem appInfo) {
final var resources = GeneratedResourcesUtils.loadFrom(generatedResources);
Deployment deployment = (Deployment) resources.stream()
.filter(Deployment.class::isInstance).findFirst()
.orElseThrow();
Expand Down

0 comments on commit 503735d

Please sign in to comment.