Repository: quarkiverse/quarkus-operator-sdk (java)

Issue:
Allow Bundle CSV annotations to be given via configuration
We can currently add annotations to the CSV using `@CSVMetadata`, but some annotations are dynamic and would be better given via configs.

For example:
- `olm.skipRange`
- `createdAt`
- `containerImage` (bundle generator could probably optionally figure this one out automatically)

Maybe a general purpose solution could be to allow configuration of arbitrary annotations via config like:
```
quarkus.operator-sdk.bundle.annotations."my-annotation"=annotation-value
```


Pull request description:
feat: provide per-bundle configuration via properties
Fixes #903

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


Patch:
diff --git a/.github/workflows/build-for-quarkus-version.yml b/.github/workflows/build-for-quarkus-version.yml
index 3c2a7355..28937829 100644
--- a/.github/workflows/build-for-quarkus-version.yml
+++ b/.github/workflows/build-for-quarkus-version.yml
@@ -252,6 +252,7 @@ jobs:
         uses: container-tools/kind-action@v2
         with:
           registry: true
+          version: v0.24.0
 
       - name: Install OPM and Operator SDK tool
         uses: redhat-actions/openshift-tools-installer@v1
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 c58d1b95..828a2810 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.Role;
 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 @@ public class BundleProcessor {
 
         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<CSVMetadataHolder, List<ReconcilerAugmentedClassInfo>>();
         ClassUtils.getKnownReconcilers(index, log)
                 .forEach(reconcilerInfo -> {
@@ -115,9 +120,19 @@ public class BundleProcessor {
                                         + "' 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 @@ public class BundleProcessor {
     }
 
     private Map<String, CSVMetadataHolder> 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 @@ public class BundleProcessor {
             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 @@ public class BundleProcessor {
         }
     }
 
-    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 @@ public class BundleProcessor {
         } 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 6058e5b5..2da9eeba 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 @@ package io.quarkiverse.operatorsdk.bundle.deployment.builders;
 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 class CsvManifestsBuilder extends ManifestsBuilder {
 
         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/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 00000000..eb0aa63b
--- /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<String, String> 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 f4106b55..f7334b68 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 @@ import io.smallrye.config.WithDefault;
 @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<String> 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<String, BundleConfiguration> 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 ed4e4d45..6635280d 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 class CSVMetadataHolder {
         public final String almExamples;
         public final String skipRange;
         public final Map<String, String> 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<String, String> others) {
@@ -57,6 +62,35 @@ public class CSVMetadataHolder {
             this.skipRange = skipRange;
             this.others = Collections.unmodifiableMap(others);
         }
+
+        public static Annotations override(Annotations initial, Map<String, String> 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<String, String> additionalAnnotationOverrides(Map<String, String> others,
+                HashMap<String, String> overrides) {
+            final var initial = new HashMap<>(others);
+            initial.putAll(overrides);
+            return initial;
+        }
+
+        private static String getOrDefault(Map<String, String> overrides, String annotation, String initialValue) {
+            final var removed = overrides.remove(annotation);
+            return removed != null ? removed : initialValue;
+        }
     }
 
     public static class Maintainer {
diff --git a/pom.xml b/pom.xml
index 8729cdb9..5942e9d9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
   <packaging>pom</packaging>
   <name>Quarkus - Operator SDK - Parent</name>
   <properties>
-    <quarkus.version>3.14.1</quarkus.version>
+    <quarkus.version>3.14.2</quarkus.version>
     <java-operator-sdk.version>4.9.4</java-operator-sdk.version>
   </properties>
   <scm>


Test patch:
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 6779fb35..fd3f921b 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 org.junit.jupiter.api.extension.RegisterExtension;
 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 @@ import io.quarkus.test.QuarkusProdModeTest;
 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 class MultipleOperatorsBundleTest {
         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 class MultipleOperatorsBundleTest {
 
         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 class MultipleOperatorsBundleTest {
         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 ddde1e81..5d222853 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;
 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 @@ import io.quarkiverse.operatorsdk.annotations.CSVMetadata.RequiredCRD;
 public class ThirdReconciler implements Reconciler<Third> {
 
     public static final String NAME = "third";
+    public static final String BUNDLE_NAME = "third-operator";
 
     @Override
     public UpdateControl<Third> reconcile(Third third, Context<Third> context) {