Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate simple generic helm chart #665

Merged
merged 42 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b456f8c
feat: generate helm chart
csviri Jun 12, 2023
ed524f8
feat: generate simple generic helm chart
csviri Jul 20, 2023
556207a
wip
csviri Jul 20, 2023
e88a95b
wip
csviri Jul 21, 2023
a3ce311
working basic helm chart
csviri Jul 24, 2023
3dea2d3
improvemets on sample chart
csviri Jul 24, 2023
0ac61d7
generate the base helm templates
csviri Jul 24, 2023
c9914e3
helm listener fix, values model
csviri Jul 25, 2023
d63d239
progress with values generation
csviri Jul 25, 2023
20bcbd9
reconciler related data generation skeleton
csviri Jul 26, 2023
a232c64
simple generation of reconilers finished
csviri Jul 26, 2023
259ae34
add crd-s to the helm chart
csviri Jul 26, 2023
6dfcbe4
config options
csviri Jul 26, 2023
87c2dab
improvements, mvp implementation works
csviri Jul 26, 2023
9b1fdba
refactoring, partially generating templates explicitly
csviri Jul 26, 2023
82cc4ff
generating role bindings for reconcilers primary
csviri Jul 27, 2023
fd741ed
fix: using qute for templating
csviri Jul 28, 2023
313b7fe
passing namespace environment variable
csviri Jul 28, 2023
c22566b
roles for dependent resources
csviri Jul 31, 2023
bfa9ffc
reusing generated Deployment
csviri Jul 31, 2023
2601ab5
helm test progress
csviri Aug 1, 2023
86bd36b
started e2e test with exposed app
csviri Aug 2, 2023
049106a
e2e progress
csviri Aug 2, 2023
0bc75a0
generate per controller namespace settings
csviri Aug 2, 2023
6970d0f
code improvements
csviri Aug 4, 2023
7ced034
rebase on main
csviri Aug 8, 2023
23b264f
e2e test improvements
csviri Aug 16, 2023
1afdad4
improvements
csviri Aug 16, 2023
2bce8a8
Merge branch 'main' into helm-chart
csviri Aug 16, 2023
9453591
remove unnecessary property
csviri Aug 17, 2023
11c6423
comment explaining class
csviri Aug 17, 2023
c2744a2
refactor: generify
metacosm Aug 17, 2023
69c4d86
remove image
csviri Aug 17, 2023
eff703f
format
csviri Aug 17, 2023
ff1128a
fixes
csviri Aug 17, 2023
c7183e5
Merge branch 'main' into helm-chart
csviri Aug 17, 2023
427b885
simple test
csviri Aug 17, 2023
ad21612
refactor: improve file management
metacosm Aug 17, 2023
4ce5e51
refactor: add utility method to deal with generated resources
metacosm Aug 17, 2023
1b10041
refactor: unify path management
metacosm Aug 17, 2023
555dabd
refactor: minor
metacosm Aug 17, 2023
e388a1b
docs: make comment more explicit
metacosm Aug 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -190,44 +185,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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comment. it is to disable default helm generator

@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