Skip to content

Commit

Permalink
feat: allow loading external CRDs from specified locations
Browse files Browse the repository at this point in the history
Locations are specified as comma-separated strings using the
quarkus.operator-sdk.bundle.external-crd-locations property.

Fixes #985

Signed-off-by: Chris Laprun <claprun@redhat.com>
  • Loading branch information
metacosm committed Nov 7, 2024
1 parent daaf418 commit 156d99c
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.quarkiverse.operatorsdk.runtime.BuildTimeOperatorConfiguration;
import io.quarkiverse.operatorsdk.runtime.CRDGenerationInfo;
import io.quarkiverse.operatorsdk.runtime.CRDInfo;
import io.quarkiverse.operatorsdk.runtime.CRDInfos;
import io.quarkiverse.operatorsdk.runtime.Version;
import io.quarkus.container.util.PathsUtil;

Expand Down Expand Up @@ -50,7 +51,7 @@ private BundleGenerator() {
public static List<ManifestsBuilder> prepareGeneration(BundleGenerationConfiguration bundleConfiguration,
BuildTimeOperatorConfiguration operatorConfiguration, Version version,
Map<CSVMetadataHolder, List<ReconcilerAugmentedClassInfo>> csvGroups, CRDGenerationInfo crds,
Path outputDirectory, String deploymentName) {
CRDInfos unownedCRDs, Path outputDirectory, String deploymentName) {
List<ManifestsBuilder> builders = new ArrayList<>();
final var mainSourcesRoot = PathsUtil.findMainSourcesRoot(outputDirectory);
final var crdNameToInfoMappings = crds.getCrds().getCRDNameToInfoMappings();
Expand All @@ -76,6 +77,10 @@ public static List<ManifestsBuilder> prepareGeneration(BundleGenerationConfigura
if (!missing.isEmpty()) {
log.warnv("Missing required CRD data for resources: {0} for bundle: {1}", missing, csvMetadata.bundleName);
}

// output non-generated CRDs
unownedCRDs.getCRDNameToInfoMappings().values()
.forEach(info -> builders.add(new CustomResourceManifestsBuilder(csvMetadata, info)));
}

return builders;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
Expand All @@ -38,12 +40,15 @@
import io.quarkiverse.operatorsdk.deployment.GeneratedCRDInfoBuildItem;
import io.quarkiverse.operatorsdk.deployment.VersionBuildItem;
import io.quarkiverse.operatorsdk.runtime.BuildTimeOperatorConfiguration;
import io.quarkiverse.operatorsdk.runtime.CRDInfo;
import io.quarkiverse.operatorsdk.runtime.CRDInfos;
import io.quarkiverse.operatorsdk.runtime.CRDUtils;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.ApplicationInfoBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.GeneratedFileSystemResourceBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.deployment.pkg.builditem.JarBuildItem;
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.kubernetes.deployment.KubernetesConfig;
Expand All @@ -58,6 +63,7 @@ public class BundleProcessor {
private static final DotName CSV_METADATA = DotName.createSimple(CSVMetadata.class.getName());
private static final String BUNDLE = "bundle";
private static final String DEFAULT_PROVIDER_NAME = System.getProperty("user.name");
private static final Set<String> EMPTY_SET = Set.of();

private static ReconcilerAugmentedClassInfo augmentReconcilerInfo(
ReconcilerAugmentedClassInfo reconcilerInfo) {
Expand Down Expand Up @@ -110,24 +116,35 @@ private static String getBundleName(AnnotationInstance csvMetadata, String defau

@BuildStep(onlyIf = IsGenerationEnabled.class)
UnownedCRDInfoBuildItem unownedCRDInfo(BundleGenerationConfiguration bundleConfiguration,
OutputTargetBuildItem outputTarget) {
CurateOutcomeBuildItem appInfoBuildItem) {
final Optional<List<String>> maybeExternalCRDs = bundleConfiguration.externalCRDLocations();
if (maybeExternalCRDs.isPresent()) {
final var moduleRoot = outputTarget.getOutputDirectory().getParent();
final var moduleRoot = appInfoBuildItem.getApplicationModel().getApplicationModule().getModuleDir().toPath();
final var crds = new CRDInfos();
maybeExternalCRDs.get().forEach(crdLocation -> {
/*
* Path crdPath = toPath(crdLocation);
* CustomResourceDefinition crd = loadFrom(crdPath);
* new CRDInfo()
*/
});
maybeExternalCRDs.get().stream()
.filter(Predicate.not(String::isBlank))
.map(String::trim)
.forEach(crdLocation -> {
final var crdPath = moduleRoot.resolve(crdLocation);
final var crd = loadFrom(crdPath);
crds.addCRDInfoFor(crd.getCrdName(), crd.getCrdSpecVersion(), crd);
});
return new UnownedCRDInfoBuildItem(crds);
} else {
return new UnownedCRDInfoBuildItem(new CRDInfos());
}
}

private CRDInfo loadFrom(Path crdPath) {
try {
final var crd = CRDUtils.loadFrom(crdPath);
final var crdName = crd.getMetadata().getName();
return new CRDInfo(crdName, CRDUtils.DEFAULT_CRD_SPEC_VERSION, crdPath.toFile().getAbsolutePath(), EMPTY_SET);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@SuppressWarnings({ "unused" })
@BuildStep(onlyIf = IsGenerationEnabled.class)
CSVMetadataBuildItem gatherCSVMetadata(KubernetesConfig kubernetesConfig,
Expand Down Expand Up @@ -231,6 +248,7 @@ void generateBundle(ApplicationInfoBuildItem configuration,
BuildTimeOperatorConfiguration operatorConfiguration,
OutputTargetBuildItem outputTarget,
CSVMetadataBuildItem csvMetadata,
UnownedCRDInfoBuildItem unownedCRDs,
VersionBuildItem versionBuildItem,
BuildProducer<GeneratedBundleBuildItem> doneGeneratingCSV,
GeneratedCRDInfoBuildItem generatedCustomResourcesDefinitions,
Expand Down Expand Up @@ -282,22 +300,24 @@ void generateBundle(ApplicationInfoBuildItem configuration,
final var generated = BundleGenerator.prepareGeneration(bundleConfiguration, operatorConfiguration,
versionBuildItem.getVersion(),
csvMetadata.getCsvGroups(), generatedCustomResourcesDefinitions.getCRDGenerationInfo(),
unownedCRDs.getCRDs(),
outputTarget.getOutputDirectory(), deploymentName);
generated.forEach(manifestBuilder -> {
final var fileName = manifestBuilder.getFileName();
final var name = manifestBuilder.getName();
try {
generatedCSVs.produce(
new GeneratedFileSystemResourceBuildItem(
Path.of(BUNDLE).resolve(manifestBuilder.getName()).resolve(fileName).toString(),
Path.of(BUNDLE).resolve(name).resolve(fileName).toString(),
manifestBuilder.getManifestData(serviceAccounts, clusterRoleBindings, clusterRoles,
roleBindings, roles, deployments)));
log.infov("Generating {0} for ''{1}'' controller -> {2}",
log.infov("Processing {0} for ''{1}'' controller -> {2}",
manifestBuilder.getManifestType(),
manifestBuilder.getName(),
outputDir.resolve(manifestBuilder.getName()).resolve(fileName));
name,
outputDir.resolve(name).resolve(fileName));
} catch (IOException e) {
log.errorv("Cannot generate {0} for ''{1}'' controller: {2}",
manifestBuilder.getManifestType(), manifestBuilder.getName(), e.getMessage());
log.errorv("Cannot process {0} for ''{1}'' controller: {2}",
manifestBuilder.getManifestType(), name, e.getMessage());
}
});
doneGeneratingCSV.produce(new GeneratedBundleBuildItem());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: externals.halkyon.io
spec:
conversion:
strategy: None
group: halkyon.io
names:
kind: External
listKind: ExternalList
plural: externals
singular: external
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
properties:
spec:
type: object
status:
type: object
type: object
served: true
storage: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: v1beta1s.test.com
spec:
group: test.com
names:
kind: V1Beta1
plural: v1beta1s
singular: v1beta1
scope: Namespaced
subresources:
status: { }
validation:
openAPIV3Schema:
properties:
spec:
properties:
value:
type: string
type: object
type: object
versions:
- name: v1
served: true
storage: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkiverse.operatorsdk.bundle;

import static io.quarkiverse.operatorsdk.bundle.Utils.*;
import static io.quarkiverse.operatorsdk.bundle.Utils.getCRDNameFor;
import static org.junit.jupiter.api.Assertions.*;

import java.io.IOException;
import java.nio.file.Files;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.quarkiverse.operatorsdk.bundle.sources.External;
import io.quarkiverse.operatorsdk.bundle.sources.ExternalDependentResource;
import io.quarkiverse.operatorsdk.bundle.sources.First;
import io.quarkiverse.operatorsdk.bundle.sources.ReconcilerWithExternalCR;
import io.quarkiverse.operatorsdk.bundle.sources.V1Beta1CRD;
import io.quarkus.test.ProdBuildResults;
import io.quarkus.test.ProdModeTestResults;
import io.quarkus.test.QuarkusProdModeTest;

public class ExternalCRDsTest {

private static final String APP = "reconciler-with-external-crds";
@RegisterExtension
static final QuarkusProdModeTest config = new QuarkusProdModeTest()
.setApplicationName(APP)
.withApplicationRoot((jar) -> jar
.addClasses(First.class, External.class, ExternalDependentResource.class,
ReconcilerWithExternalCR.class))
.overrideConfigKey("quarkus.operator-sdk.bundle.external-crd-locations",
"src/test/external-crds/v1beta1spec.crd.yml, src/test/external-crds/external.crd.yml");

@ProdBuildResults
private ProdModeTestResults prodModeTestResults;

@Test
public void shouldProcessExternalCRDsWhenPresentAndOutputWarningsAsNeeded() throws IOException {
final var bundle = prodModeTestResults.getBuildDir().resolve(BUNDLE);
assertTrue(Files.exists(bundle.resolve(APP)));
final var manifests = bundle.resolve(APP).resolve("manifests");
assertFileExistsIn(manifests.resolve(getCRDNameFor(External.class)), manifests);
assertFileExistsIn(manifests.resolve(getCRDNameFor(V1Beta1CRD.class)), manifests);

final var csv = getCSVFor(bundle, APP);
final var externalCRD = csv.getSpec().getCustomresourcedefinitions().getRequired().get(0);
assertEquals(HasMetadata.getFullResourceName(External.class), externalCRD.getName());
final var v1beta1 = csv.getSpec().getCustomresourcedefinitions().getRequired().get(1);
assertEquals(HasMetadata.getFullResourceName(V1Beta1CRD.class), v1beta1.getName());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ public void shouldWriteBundleForTheOperators() throws IOException {
assertEquals(Third.DISPLAY, thirdCRD.getDisplayName());
assertEquals(Third.DESCRIPTION, thirdCRD.getDescription());
// CRDs should be alphabetically ordered
final var externalCRD = crds.getRequired().get(0);
final var externalCRD = crds.getRequired().get(1);
assertEquals(HasMetadata.getFullResourceName(External.class), externalCRD.getName());
assertEquals(External.DISPLAY_NAME, externalCRD.getDisplayName());
assertEquals(External.DESCRIPTION, externalCRD.getDescription());
assertEquals(HasMetadata.getFullResourceName(SecondExternal.class), crds.getRequired().get(1).getName());
assertEquals(HasMetadata.getFullResourceName(SecondExternal.class), crds.getRequired().get(0).getName());
// should list native APIs as well
final var spec = csv.getSpec();
final var nativeAPIs = spec.getNativeAPIs();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkiverse.operatorsdk.bundle.sources;

import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.quarkiverse.operatorsdk.annotations.CSVMetadata;

@ControllerConfiguration(dependents = @Dependent(type = ExternalDependentResource.class))
@CSVMetadata(requiredCRDs = @CSVMetadata.RequiredCRD(kind = V1Beta1CRD.KIND, name = V1Beta1CRD.CR_NAME, version = V1Beta1CRD.VERSION))
public class ReconcilerWithExternalCR implements Reconciler<First> {
@Override
public UpdateControl<First> reconcile(First first, Context<First> context) throws Exception {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkiverse.operatorsdk.bundle.sources;

import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Kind;
import io.fabric8.kubernetes.model.annotation.Version;

@Group(V1Beta1CRD.GROUP)
@Version(V1Beta1CRD.VERSION)
@Kind(V1Beta1CRD.KIND)
public class V1Beta1CRD extends CustomResource<V1Beta1CRD.Spec, Void> {

public static final String GROUP = "test.com";
public static final String VERSION = "v2";
public static final String KIND = "V1Beta1";
public static final String CR_NAME = "v1beta1s." + GROUP;

public record Spec(String value) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;

public class CRDInfos {
private final Map<String, Map<String, CRDInfo>> infos;
private final static String CRD_SPEC_VERSION = HasMetadata.getVersion(CustomResourceDefinition.class);

public CRDInfos() {
this(new HashMap<>());
Expand All @@ -33,15 +29,15 @@ public Map<String, CRDInfo> getCRDNameToInfoMappings() {
.values().stream()
// only keep CRD v1 infos
.flatMap(entry -> entry.values().stream()
.filter(crdInfo -> CRD_SPEC_VERSION.equals(crdInfo.getCrdSpecVersion())))
.filter(crdInfo -> CRDUtils.DEFAULT_CRD_SPEC_VERSION.equals(crdInfo.getCrdSpecVersion())))
.collect(Collectors.toMap(CRDInfo::getCrdName, Function.identity()));
}

public Map<String, Map<String, CRDInfo>> getExisting() {
return infos;
}

public void addCRDInfoFor(String crdName, String version, CRDInfo crdInfo) {
getOrCreateCRDSpecVersionToInfoMapping(crdName).put(version, crdInfo);
public void addCRDInfoFor(String crdName, String crdSpecVersion, CRDInfo crdInfo) {
getOrCreateCRDSpecVersionToInfoMapping(crdName).put(crdSpecVersion, crdInfo);
}
}
Loading

0 comments on commit 156d99c

Please sign in to comment.