Skip to content

Commit

Permalink
feat: generate simple generic helm chart (#665)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Laprun <[email protected]>
  • Loading branch information
csviri and metacosm authored Aug 17, 2023
1 parent bddef54 commit 1515cb7
Show file tree
Hide file tree
Showing 27 changed files with 908 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static io.quarkiverse.operatorsdk.deployment.AddClusterRolesDecorator.ALL_VERBS;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -22,7 +21,6 @@
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;

import io.dekorate.utils.Serialization;
import io.fabric8.kubernetes.api.model.ServiceAccount;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.rbac.ClusterRole;
Expand All @@ -34,10 +32,7 @@
import io.quarkiverse.operatorsdk.bundle.runtime.CSVMetadata.Icon;
import io.quarkiverse.operatorsdk.bundle.runtime.CSVMetadataHolder;
import io.quarkiverse.operatorsdk.bundle.runtime.SharedCSVMetadata;
import io.quarkiverse.operatorsdk.common.ClassUtils;
import io.quarkiverse.operatorsdk.common.ConfigurationUtils;
import io.quarkiverse.operatorsdk.common.ReconciledAugmentedClassInfo;
import io.quarkiverse.operatorsdk.common.ReconcilerAugmentedClassInfo;
import io.quarkiverse.operatorsdk.common.*;
import io.quarkiverse.operatorsdk.deployment.GeneratedCRDInfoBuildItem;
import io.quarkiverse.operatorsdk.deployment.VersionBuildItem;
import io.quarkiverse.operatorsdk.runtime.CRDInfo;
Expand Down Expand Up @@ -192,44 +187,37 @@ void generateBundle(ApplicationInfoBuildItem configuration,
final var roles = new LinkedList<Role>();
final var deployments = new LinkedList<Deployment>();

generatedKubernetesManifests.stream()
.filter(bi -> bi.getName().equals("kubernetes.yml"))
.findAny()
.ifPresent(
bi -> {
final var resources = Serialization
.unmarshalAsList(new ByteArrayInputStream(bi.getContent()));
resources.getItems().forEach(r -> {
if (r instanceof ServiceAccount) {
serviceAccounts.add((ServiceAccount) r);
return;
}

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

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

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

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

if (r instanceof Deployment) {
deployments.add((Deployment) r);
}
});
});
final var resources = GeneratedResourcesUtils.loadFrom(generatedKubernetesManifests);
resources.forEach(r -> {
if (r instanceof ServiceAccount) {
serviceAccounts.add((ServiceAccount) r);
return;
}

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

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

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

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

if (r instanceof Deployment) {
deployments.add((Deployment) r);
}
});
final var generated = BundleGenerator.prepareGeneration(bundleConfiguration, versionBuildItem.getVersion(),
csvMetadata.getCsvGroups(), crds, outputTarget.getOutputDirectory());
generated.forEach(manifestBuilder -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkiverse.operatorsdk.common;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.List;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;

public class FileUtils {
private final static KubernetesSerialization serializer = new KubernetesSerialization();

public static void ensureDirectoryExists(File dir) {
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new IllegalArgumentException("Couldn't create " + dir.getAbsolutePath());
}
}
}

public static List<HasMetadata> unmarshalFrom(byte[] yamlOrJson) {
return serializer.unmarshal(new ByteArrayInputStream(yamlOrJson));
}

public static String asYaml(Object toSerialize) {
return serializer.asYaml(toSerialize);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkiverse.operatorsdk.common;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import org.jboss.logging.Logger;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.quarkus.kubernetes.spi.GeneratedKubernetesResourceBuildItem;

public class GeneratedResourcesUtils {
public static final String KUBERNETES_YAML = "kubernetes.yml";
private static final Logger log = Logger.getLogger(GeneratedResourcesUtils.class.getName());

public static List<HasMetadata> loadFrom(List<GeneratedKubernetesResourceBuildItem> generatedResources,
String resourceName) {
if (generatedResources.isEmpty()) {
log.debugv("Couldn't load resource {0} because no resources were generated", resourceName);
return Collections.emptyList();
}
var buildItem = generatedResources.stream()
.filter(r -> resourceName.equals(r.getName()))
.findAny();
return buildItem.map(bi -> FileUtils.unmarshalFrom(bi.getContent()))
.orElseThrow(() -> new IllegalArgumentException("Couldn't find resource " + resourceName +
" in generated resources: " + generatedResources.stream()
.map(GeneratedKubernetesResourceBuildItem::getName).collect(Collectors.toSet())));
}

public static List<HasMetadata> loadFrom(List<GeneratedKubernetesResourceBuildItem> generatedResources) {
return loadFrom(generatedResources, KUBERNETES_YAML);
}
}
15 changes: 15 additions & 0 deletions core/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@
<artifactId>semver4j</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>io.dekorate</groupId>
<artifactId>helm-annotations</artifactId>
<classifier>noapt</classifier>
<exclusions>
<exclusion>
<groupId>io.sundr</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.dekorate</groupId>
<artifactId>kubernetes-annotations</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,59 +51,8 @@ public AddClusterRolesDecorator(Collection<QuarkusControllerConfiguration> confi
@Override
public void visit(KubernetesListBuilder list) {
configs.forEach(cri -> {
final var rule = new PolicyRuleBuilder();
final var resourceClass = cri.getResourceClass();
final var plural = HasMetadata.getPlural(resourceClass);
rule.addToResources(plural);

// if the resource has a non-Void status, also add the status resource
if (cri.isStatusPresentAndNotVoid()) {
rule.addToResources(plural + "/status");
}

// add finalizers sub-resource because it's used in several contexts, even in the absence of finalizers
// see: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement
rule.addToResources(plural + "/finalizers");

rule.addToApiGroups(HasMetadata.getGroup(resourceClass))
.addToVerbs(ALL_VERBS)
.build();

final var clusterRoleBuilder = new ClusterRoleBuilder()
.withNewMetadata()
.withName(getClusterRoleName(cri.getName()))
.endMetadata()
.addToRules(rule.build());

@SuppressWarnings({ "rawtypes", "unchecked" })
final Map<String, DependentResourceSpecMetadata> dependentsMetadata = cri.getDependentsMetadata();
dependentsMetadata.forEach((name, spec) -> {
final var dependentResourceClass = spec.getDependentResourceClass();
final var associatedResourceClass = spec.getDependentType();

// only process Kubernetes dependents
if (HasMetadata.class.isAssignableFrom(associatedResourceClass)) {
final var dependentRule = new PolicyRuleBuilder()
.addToApiGroups(HasMetadata.getGroup(associatedResourceClass))
.addToResources(HasMetadata.getPlural(associatedResourceClass))
.addToVerbs(READ_VERBS);
if (Updater.class.isAssignableFrom(dependentResourceClass)) {
dependentRule.addToVerbs(UPDATE_VERBS);
}
if (Deleter.class.isAssignableFrom(dependentResourceClass)) {
dependentRule.addToVerbs(DELETE_VERB);
}
if (Creator.class.isAssignableFrom(dependentResourceClass)) {
dependentRule.addToVerbs(CREATE_VERB);
if (!dependentRule.getVerbs().contains(PATCH_VERB)) {
dependentRule.addToVerbs(PATCH_VERB);
}
}
clusterRoleBuilder.addToRules(dependentRule.build());
}
});

list.addToItems(clusterRoleBuilder.build());
var clusterRole = createClusterRole(cri);
list.addToItems(clusterRole);
});

// if we're asking to validate the CRDs, also add CRDs permissions, once
Expand All @@ -121,6 +70,61 @@ public void visit(KubernetesListBuilder list) {
}
}

public static ClusterRole createClusterRole(QuarkusControllerConfiguration<?> cri) {
final var rule = new PolicyRuleBuilder();
final var resourceClass = cri.getResourceClass();
final var plural = HasMetadata.getPlural(resourceClass);
rule.addToResources(plural);

// if the resource has a non-Void status, also add the status resource
if (cri.isStatusPresentAndNotVoid()) {
rule.addToResources(plural + "/status");
}

// add finalizers sub-resource because it's used in several contexts, even in the absence of finalizers
// see: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement
rule.addToResources(plural + "/finalizers");

rule.addToApiGroups(HasMetadata.getGroup(resourceClass))
.addToVerbs(ALL_VERBS)
.build();

final var clusterRoleBuilder = new ClusterRoleBuilder()
.withNewMetadata()
.withName(getClusterRoleName(cri.getName()))
.endMetadata()
.addToRules(rule.build());

final Map<String, DependentResourceSpecMetadata<?, ?, ?>> dependentsMetadata = cri.getDependentsMetadata();
dependentsMetadata.forEach((name, spec) -> {
final var dependentResourceClass = spec.getDependentResourceClass();
final var associatedResourceClass = spec.getDependentType();

// only process Kubernetes dependents
if (HasMetadata.class.isAssignableFrom(associatedResourceClass)) {
final var dependentRule = new PolicyRuleBuilder()
.addToApiGroups(HasMetadata.getGroup(associatedResourceClass))
.addToResources(HasMetadata.getPlural(associatedResourceClass))
.addToVerbs(READ_VERBS);
if (Updater.class.isAssignableFrom(dependentResourceClass)) {
dependentRule.addToVerbs(UPDATE_VERBS);
}
if (Deleter.class.isAssignableFrom(dependentResourceClass)) {
dependentRule.addToVerbs(DELETE_VERB);
}
if (Creator.class.isAssignableFrom(dependentResourceClass)) {
dependentRule.addToVerbs(CREATE_VERB);
if (!dependentRule.getVerbs().contains(PATCH_VERB)) {
dependentRule.addToVerbs(PATCH_VERB);
}
}
clusterRoleBuilder.addToRules(dependentRule.build());
}

});
return clusterRoleBuilder.build();
}

public static String getClusterRoleName(String controller) {
return controller + "-cluster-role";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.fabric8.crd.generator.CustomResourceInfo;
import io.fabric8.kubernetes.client.CustomResource;
import io.quarkiverse.operatorsdk.common.CustomResourceAugmentedClassInfo;
import io.quarkiverse.operatorsdk.common.FileUtils;
import io.quarkiverse.operatorsdk.runtime.CRDConfiguration;
import io.quarkiverse.operatorsdk.runtime.CRDGenerationInfo;
import io.quarkiverse.operatorsdk.runtime.CRDInfo;
Expand Down Expand Up @@ -76,11 +77,7 @@ CRDGenerationInfo generate(OutputTargetBuildItem outputTarget,
.map(d -> Paths.get("").toAbsolutePath().resolve(d))
.orElse(outputTarget.getOutputDirectory().resolve(KUBERNETES));
final var outputDir = targetDirectory.toFile();
if (!outputDir.exists()) {
if (!outputDir.mkdirs()) {
throw new IllegalArgumentException("Couldn't create " + outputDir.getAbsolutePath());
}
}
FileUtils.ensureDirectoryExists(outputDir);

// generate CRDs with detailed information
final var info = generator.forCRDVersions(crdConfiguration.versions).inOutputDir(outputDir).detailedGenerate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import io.quarkiverse.operatorsdk.common.ReconcilerAugmentedClassInfo;
import io.quarkus.builder.item.SimpleBuildItem;

final class ReconcilerInfosBuildItem extends SimpleBuildItem {
public final class ReconcilerInfosBuildItem extends SimpleBuildItem {
private final Map<String, ReconcilerAugmentedClassInfo> reconcilers;

public ReconcilerInfosBuildItem(Map<String, ReconcilerAugmentedClassInfo> reconcilers) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkiverse.operatorsdk.deployment.helm;

import java.util.HashMap;
import java.util.Map;

import io.dekorate.WithSession;
import io.dekorate.kubernetes.config.BaseConfigFluent;
import io.dekorate.kubernetes.config.Configurator;

/**
* Used to disable default Dekorate Helm chart generator, which would get automatically triggered by depending on the Dekorate
* Helm annotations and the Quarkus Kubernetes extension.
*/
public class DisableDefaultHelmListener extends Configurator<BaseConfigFluent<?>> implements WithSession {
@Override
public void visit(BaseConfigFluent<?> baseConfigFluent) {
Map<String, Object> helmConfig = new HashMap<>();
helmConfig.put("enabled", "false");

Map<String, Object> config = new HashMap<>();
config.put("helm", helmConfig);

WithSession.super.getSession().addPropertyConfiguration(config);
}
}
Loading

0 comments on commit 1515cb7

Please sign in to comment.