From c3a1376112555985716873a3b14b8f788cb83fc5 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 5 Sep 2024 10:16:34 +0200 Subject: [PATCH] feat: provide per-bundle configuration via properties Fixes #903 Signed-off-by: Chris Laprun --- .../bundle/deployment/BundleProcessor.java | 33 ++++++++++----- .../builders/CsvManifestsBuilder.java | 15 +++---- .../bundle/MultipleOperatorsBundleTest.java | 41 ++++++++++++++++--- .../bundle/sources/ThirdReconciler.java | 5 ++- .../bundle/runtime/BundleConfiguration.java | 28 +++++++++++++ .../BundleGenerationConfiguration.java | 11 +++++ .../bundle/runtime/CSVMetadataHolder.java | 34 +++++++++++++++ 7 files changed, 142 insertions(+), 25 deletions(-) create mode 100644 bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/BundleConfiguration.java diff --git a/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/BundleProcessor.java b/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/BundleProcessor.java index c58d1b95f..828a2810a 100644 --- a/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/BundleProcessor.java +++ b/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/BundleProcessor.java @@ -29,6 +29,7 @@ import io.fabric8.kubernetes.api.model.rbac.RoleBinding; import io.quarkiverse.operatorsdk.annotations.CSVMetadata; import io.quarkiverse.operatorsdk.annotations.SharedCSVMetadata; +import io.quarkiverse.operatorsdk.bundle.runtime.BundleConfiguration; import io.quarkiverse.operatorsdk.bundle.runtime.BundleGenerationConfiguration; import io.quarkiverse.operatorsdk.bundle.runtime.CSVMetadataHolder; import io.quarkiverse.operatorsdk.common.*; @@ -85,7 +86,11 @@ CSVMetadataBuildItem gatherCSVMetadata(KubernetesConfig kubernetesConfig, final var defaultReplaces = bundleConfiguration.replaces().orElse(null); - final var sharedMetadataHolders = getSharedMetadataHolders(defaultName, defaultVersion, defaultReplaces, index); + final var bundleConfigs = bundleConfiguration.bundles(); + final var defaultBundleConfig = bundleConfigs.get(BundleGenerationConfiguration.DEFAULT_BUNDLE_NAME); + + final var sharedMetadataHolders = getSharedMetadataHolders(defaultName, defaultVersion, defaultReplaces, + defaultBundleConfig, index); final var csvGroups = new HashMap>(); ClassUtils.getKnownReconcilers(index, log) .forEach(reconcilerInfo -> { @@ -115,9 +120,19 @@ CSVMetadataBuildItem gatherCSVMetadata(KubernetesConfig kubernetesConfig, + "' but no SharedCSVMetadata implementation with that name exists. Please create a SharedCSVMetadata with that name to have one single source of truth and reference it via CSVMetadata annotations using that name on your reconcilers."); } } + + // merge default bundle configuration with more specific one if they exist + var bundleConfig = bundleConfigs.get(sharedMetadataName); + if (bundleConfig != null) { + bundleConfig.mergeWithDefaults(defaultBundleConfig); + } else { + bundleConfig = defaultBundleConfig; + } + csvMetadata = createMetadataHolder(csvMetadataAnnotation, new CSVMetadataHolder(sharedMetadataName, defaultVersion, defaultReplaces, - DEFAULT_PROVIDER_NAME, origin)); + DEFAULT_PROVIDER_NAME, origin), + bundleConfig, origin); if (DEFAULT_PROVIDER_NAME.equals(csvMetadata.providerName)) { log.warnv( "It is recommended that you provide a provider name provided for {0}: ''{1}'' was used as default value.", @@ -265,7 +280,7 @@ void generateBundle(ApplicationInfoBuildItem configuration, } private Map getSharedMetadataHolders(String name, String version, String defaultReplaces, - IndexView index) { + BundleConfiguration defaultBundleConfig, IndexView index) { CSVMetadataHolder csvMetadata = new CSVMetadataHolder(name, version, defaultReplaces, DEFAULT_PROVIDER_NAME, "default"); final var sharedMetadataImpls = index.getAllKnownImplementors(SHARED_CSV_METADATA); @@ -274,7 +289,7 @@ private Map getSharedMetadataHolders(String name, Str final var csvMetadataAnn = sharedMetadataImpl.declaredAnnotation(CSV_METADATA); if (csvMetadataAnn != null) { final var origin = sharedMetadataImpl.name().toString(); - final var metadataHolder = createMetadataHolder(csvMetadataAnn, csvMetadata, origin); + final var metadataHolder = createMetadataHolder(csvMetadataAnn, csvMetadata, defaultBundleConfig, origin); final var existing = result.get(metadataHolder.bundleName); if (existing != null) { throw new IllegalStateException( @@ -302,13 +317,8 @@ private static String getBundleName(AnnotationInstance csvMetadata, String defau } } - private CSVMetadataHolder createMetadataHolder(AnnotationInstance csvMetadata, - CSVMetadataHolder mh) { - return createMetadataHolder(csvMetadata, mh, mh.getOrigin()); - } - private CSVMetadataHolder createMetadataHolder(AnnotationInstance csvMetadata, CSVMetadataHolder mh, - String origin) { + BundleConfiguration bundleConfig, String origin) { if (csvMetadata == null) { return mh; } @@ -357,6 +367,9 @@ private CSVMetadataHolder createMetadataHolder(AnnotationInstance csvMetadata, C } else { annotations = mh.annotations; } + if (bundleConfig != null) { + annotations = CSVMetadataHolder.Annotations.override(annotations, bundleConfig.annotations()); + } final var maintainersField = csvMetadata.value("maintainers"); CSVMetadataHolder.Maintainer[] maintainers; diff --git a/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/builders/CsvManifestsBuilder.java b/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/builders/CsvManifestsBuilder.java index 6058e5b51..2da9eebab 100644 --- a/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/builders/CsvManifestsBuilder.java +++ b/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/builders/CsvManifestsBuilder.java @@ -3,6 +3,7 @@ import static io.quarkiverse.operatorsdk.bundle.deployment.BundleGenerator.MANIFESTS; import static io.quarkiverse.operatorsdk.bundle.deployment.BundleProcessor.CRD_DESCRIPTION; import static io.quarkiverse.operatorsdk.bundle.deployment.BundleProcessor.CRD_DISPLAY_NAME; +import static io.quarkiverse.operatorsdk.bundle.runtime.BundleConfiguration.*; import static java.util.Comparator.comparing; import java.io.FileInputStream; @@ -85,13 +86,13 @@ public CsvManifestsBuilder(CSVMetadataHolder metadata, BuildTimeOperatorConfigur final var metadataBuilder = csvBuilder.withNewMetadata().withName(metadata.csvName); if (metadata.annotations != null) { - metadataBuilder.addToAnnotations("olm.skipRange", metadata.annotations.skipRange); - metadataBuilder.addToAnnotations("containerImage", metadata.annotations.containerImage); - metadataBuilder.addToAnnotations("repository", metadata.annotations.repository); - metadataBuilder.addToAnnotations("capabilities", metadata.annotations.capabilities); - metadataBuilder.addToAnnotations("categories", metadata.annotations.categories); - metadataBuilder.addToAnnotations("certified", String.valueOf(metadata.annotations.certified)); - metadataBuilder.addToAnnotations("alm-examples", metadata.annotations.almExamples); + metadataBuilder.addToAnnotations(OLM_SKIP_RANGE_ANNOTATION, metadata.annotations.skipRange); + metadataBuilder.addToAnnotations(CONTAINER_IMAGE_ANNOTATION, metadata.annotations.containerImage); + metadataBuilder.addToAnnotations(REPOSITORY_ANNOTATION, metadata.annotations.repository); + metadataBuilder.addToAnnotations(CAPABILITIES_ANNOTATION, metadata.annotations.capabilities); + metadataBuilder.addToAnnotations(CATEGORIES_ANNOTATION, metadata.annotations.categories); + metadataBuilder.addToAnnotations(CERTIFIED_ANNOTATION, String.valueOf(metadata.annotations.certified)); + metadataBuilder.addToAnnotations(ALM_EXAMPLES_ANNOTATION, metadata.annotations.almExamples); if (metadata.annotations.others != null) { metadata.annotations.others.forEach(metadataBuilder::addToAnnotations); } diff --git a/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/MultipleOperatorsBundleTest.java b/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/MultipleOperatorsBundleTest.java index 6779fb35a..fd3f921bf 100644 --- a/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/MultipleOperatorsBundleTest.java +++ b/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/MultipleOperatorsBundleTest.java @@ -12,6 +12,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.rbac.PolicyRuleBuilder; +import io.quarkiverse.operatorsdk.bundle.runtime.BundleConfiguration; +import io.quarkiverse.operatorsdk.bundle.runtime.BundleGenerationConfiguration; import io.quarkiverse.operatorsdk.bundle.sources.*; import io.quarkiverse.operatorsdk.common.ConfigurationUtils; import io.quarkus.test.ProdBuildResults; @@ -21,7 +23,13 @@ public class MultipleOperatorsBundleTest { private static final String VERSION = "test-version"; - public static final String BUNDLE_PACKAGE = "olm-package"; + private static final String BUNDLE_PACKAGE = "olm-package"; + private static final String OVERRIDEN_REPO_ANNOTATION = "overridden-repo-annotation"; + private static final String DEFAULT_ANNOTATION_NAME = "default-annotation-name"; + private static final String DEFAULT_ANNOTATION_VALUE = "default-annotation-value"; + private static final String OVERRIDDEN_DEFAULT_ANNOTATION_NAME = "overridden-annotation-name"; + private static final String OVERRIDEN_DEFAULT_ANNOTATION_VALUE = "initial-annotation-value"; + private static final String OVERRIDEN_BY_THIRD_ANNOTATION_VALUE = "overridden-by-third-annotation-value"; @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() @@ -33,7 +41,19 @@ public class MultipleOperatorsBundleTest { ExternalDependentResource.class, PodDependentResource.class)) .overrideConfigKey("quarkus.operator-sdk.crd.generate-all", "true") .overrideConfigKey("quarkus.operator-sdk.bundle.replaces", FirstReconciler.REPLACES) - .overrideConfigKey("quarkus.operator-sdk.bundle.package-name", BUNDLE_PACKAGE); + .overrideConfigKey("quarkus.operator-sdk.bundle.package-name", BUNDLE_PACKAGE) + .overrideConfigKey("quarkus.operator-sdk.bundle.bundles." + ThirdReconciler.BUNDLE_NAME + ".annotations." + + BundleConfiguration.REPOSITORY_ANNOTATION, OVERRIDEN_REPO_ANNOTATION) + .overrideConfigKey( + "quarkus.operator-sdk.bundle.bundles." + BundleGenerationConfiguration.DEFAULT_BUNDLE_NAME + ".annotations." + + DEFAULT_ANNOTATION_NAME, + DEFAULT_ANNOTATION_VALUE) + .overrideConfigKey( + "quarkus.operator-sdk.bundle.bundles." + BundleGenerationConfiguration.DEFAULT_BUNDLE_NAME + ".annotations." + + OVERRIDDEN_DEFAULT_ANNOTATION_NAME, + OVERRIDEN_DEFAULT_ANNOTATION_VALUE) + .overrideConfigKey("quarkus.operator-sdk.bundle.bundles." + ThirdReconciler.BUNDLE_NAME + ".annotations." + + OVERRIDDEN_DEFAULT_ANNOTATION_NAME, OVERRIDEN_BY_THIRD_ANNOTATION_VALUE); @SuppressWarnings("unused") @ProdBuildResults @@ -45,6 +65,9 @@ public void shouldWriteBundleForTheOperators() throws IOException { checkBundleFor(bundle, "first-operator", First.class); // check that version is properly overridden var csv = getCSVFor(bundle, "first-operator"); + var metadata = csv.getMetadata(); + var annotations = metadata.getAnnotations(); + assertEquals(OVERRIDEN_DEFAULT_ANNOTATION_VALUE, annotations.get(OVERRIDDEN_DEFAULT_ANNOTATION_NAME)); assertEquals(FirstReconciler.VERSION, csv.getSpec().getVersion()); assertEquals(FirstReconciler.REPLACES, csv.getSpec().getReplaces()); var bundleMeta = getAnnotationsFor(bundle, "first-operator"); @@ -52,6 +75,9 @@ public void shouldWriteBundleForTheOperators() throws IOException { checkBundleFor(bundle, "second-operator", Second.class); csv = getCSVFor(bundle, "second-operator"); + metadata = csv.getMetadata(); + annotations = metadata.getAnnotations(); + assertEquals(OVERRIDEN_DEFAULT_ANNOTATION_VALUE, annotations.get(OVERRIDDEN_DEFAULT_ANNOTATION_NAME)); final var permissions = csv.getSpec().getInstall().getSpec().getPermissions(); assertEquals(1, permissions.size()); assertTrue(permissions.get(0).getRules().contains(new PolicyRuleBuilder() @@ -86,10 +112,13 @@ public void shouldWriteBundleForTheOperators() throws IOException { assertEquals(HasMetadata.getKind(Pod.class), podGVK.getKind()); assertEquals(HasMetadata.getVersion(Pod.class), podGVK.getVersion()); assertEquals("1.0.0", spec.getReplaces()); - final var metadata = csv.getMetadata(); - assertEquals(">=1.0.0 <1.0.3", metadata.getAnnotations().get("olm.skipRange")); - assertEquals("Test", metadata.getAnnotations().get("capabilities")); - assertEquals("bar", metadata.getAnnotations().get("foo")); + metadata = csv.getMetadata(); + annotations = metadata.getAnnotations(); + assertEquals(">=1.0.0 <1.0.3", annotations.get(BundleConfiguration.OLM_SKIP_RANGE_ANNOTATION)); + assertEquals("Test", annotations.get(BundleConfiguration.CAPABILITIES_ANNOTATION)); + assertEquals(OVERRIDEN_REPO_ANNOTATION, annotations.get(BundleConfiguration.REPOSITORY_ANNOTATION)); + assertEquals(OVERRIDEN_BY_THIRD_ANNOTATION_VALUE, annotations.get(OVERRIDDEN_DEFAULT_ANNOTATION_NAME)); + assertEquals("bar", annotations.get("foo")); // version should be the default application's version since it's not provided for this reconciler assertEquals(VERSION, spec.getVersion()); diff --git a/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/sources/ThirdReconciler.java b/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/sources/ThirdReconciler.java index ddde1e81d..5d222853b 100644 --- a/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/sources/ThirdReconciler.java +++ b/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/sources/ThirdReconciler.java @@ -10,8 +10,8 @@ import io.quarkiverse.operatorsdk.annotations.CSVMetadata.Annotations.Annotation; import io.quarkiverse.operatorsdk.annotations.CSVMetadata.RequiredCRD; -@CSVMetadata(bundleName = "third-operator", requiredCRDs = @RequiredCRD(kind = SecondExternal.KIND, name = "externalagains." - + SecondExternal.GROUP, version = SecondExternal.VERSION), replaces = "1.0.0", annotations = @Annotations(skipRange = ">=1.0.0 <1.0.3", capabilities = "Test", others = @Annotation(name = "foo", value = "bar"))) +@CSVMetadata(bundleName = ThirdReconciler.BUNDLE_NAME, requiredCRDs = @RequiredCRD(kind = SecondExternal.KIND, name = "externalagains." + + SecondExternal.GROUP, version = SecondExternal.VERSION), replaces = "1.0.0", annotations = @Annotations(skipRange = ">=1.0.0 <1.0.3", capabilities = "Test", repository = "should be overridden by property", others = @Annotation(name = "foo", value = "bar"))) @ControllerConfiguration(name = ThirdReconciler.NAME, dependents = { @Dependent(type = ExternalDependentResource.class), @Dependent(name = "pod1", type = PodDependentResource.class), @@ -20,6 +20,7 @@ public class ThirdReconciler implements Reconciler { public static final String NAME = "third"; + public static final String BUNDLE_NAME = "third-operator"; @Override public UpdateControl reconcile(Third third, Context context) { diff --git a/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/BundleConfiguration.java b/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/BundleConfiguration.java new file mode 100644 index 000000000..eb0aa63b5 --- /dev/null +++ b/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/BundleConfiguration.java @@ -0,0 +1,28 @@ +package io.quarkiverse.operatorsdk.bundle.runtime; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; + +@ConfigGroup +public interface BundleConfiguration { + String OLM_SKIP_RANGE_ANNOTATION = "olm.skipRange"; + String CONTAINER_IMAGE_ANNOTATION = "containerImage"; + String REPOSITORY_ANNOTATION = "repository"; + String CAPABILITIES_ANNOTATION = "capabilities"; + String CATEGORIES_ANNOTATION = "categories"; + String CERTIFIED_ANNOTATION = "certified"; + String ALM_EXAMPLES_ANNOTATION = "alm-examples"; + + /** + * The bundle's annotations (as found in the CSV metadata) + */ + Map annotations(); + + default void mergeWithDefaults(BundleConfiguration defaults) { + final var annotations = annotations(); + if (annotations != null) { + annotations.keySet().forEach(key -> annotations.computeIfAbsent(key, k -> defaults.annotations().get(k))); + } + } +} diff --git a/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/BundleGenerationConfiguration.java b/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/BundleGenerationConfiguration.java index f4106b556..f7334b682 100644 --- a/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/BundleGenerationConfiguration.java +++ b/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/BundleGenerationConfiguration.java @@ -1,6 +1,7 @@ package io.quarkiverse.operatorsdk.bundle.runtime; import java.util.List; +import java.util.Map; import java.util.Optional; import io.quarkiverse.operatorsdk.annotations.CSVMetadata; @@ -11,6 +12,8 @@ @ConfigMapping(prefix = "quarkus.operator-sdk.bundle") @ConfigRoot public interface BundleGenerationConfiguration { + String DEFAULT_BUNDLE_NAME = "QOSDK_DEFAULT"; + /** * Whether the extension should generate the Operator bundle. */ @@ -47,4 +50,12 @@ public interface BundleGenerationConfiguration { */ Optional version(); + /** + * Per-bundle configuration. Note that you can also provide default values that will be applied to all your bundles by + * adding configuration using the {@link #DEFAULT_BUNDLE_NAME} key. In that case, any configuration found under that key + * will be used as default for every bundle unless otherwise overridden. + * + * @since 6.8.0 + */ + Map bundles(); } diff --git a/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/CSVMetadataHolder.java b/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/CSVMetadataHolder.java index ed4e4d454..6635280de 100644 --- a/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/CSVMetadataHolder.java +++ b/bundle-generator/runtime/src/main/java/io/quarkiverse/operatorsdk/bundle/runtime/CSVMetadataHolder.java @@ -1,6 +1,10 @@ package io.quarkiverse.operatorsdk.bundle.runtime; +import static io.quarkiverse.operatorsdk.bundle.runtime.BundleConfiguration.*; +import static io.quarkiverse.operatorsdk.bundle.runtime.BundleConfiguration.CONTAINER_IMAGE_ANNOTATION; + import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -45,6 +49,7 @@ public static class Annotations { public final String almExamples; public final String skipRange; public final Map others; + private final static Annotations EMPTY = new Annotations(null, null, null, null, false, null, null, Map.of()); public Annotations(String containerImage, String repository, String capabilities, String categories, boolean certified, String almExamples, String skipRange, Map others) { @@ -57,6 +62,35 @@ public Annotations(String containerImage, String repository, String capabilities this.skipRange = skipRange; this.others = Collections.unmodifiableMap(others); } + + public static Annotations override(Annotations initial, Map overrides) { + if (initial == null) { + initial = EMPTY; + } + final var copy = new HashMap<>(overrides); + + return new Annotations( + getOrDefault(copy, CONTAINER_IMAGE_ANNOTATION, initial.containerImage), + getOrDefault(copy, REPOSITORY_ANNOTATION, initial.repository), + getOrDefault(copy, CAPABILITIES_ANNOTATION, initial.capabilities), + getOrDefault(copy, CATEGORIES_ANNOTATION, initial.categories), + Boolean.parseBoolean(getOrDefault(copy, CERTIFIED_ANNOTATION, "false")), + getOrDefault(copy, ALM_EXAMPLES_ANNOTATION, initial.almExamples), + getOrDefault(copy, OLM_SKIP_RANGE_ANNOTATION, initial.skipRange), + additionalAnnotationOverrides(initial.others, copy)); + } + + private static Map additionalAnnotationOverrides(Map others, + HashMap overrides) { + final var initial = new HashMap<>(others); + initial.putAll(overrides); + return initial; + } + + private static String getOrDefault(Map overrides, String annotation, String initialValue) { + final var removed = overrides.remove(annotation); + return removed != null ? removed : initialValue; + } } public static class Maintainer {