diff --git a/.github/workflows/build-for-quarkus-version.yml b/.github/workflows/build-for-quarkus-version.yml index f7d70c7ce..d99b9d72a 100644 --- a/.github/workflows/build-for-quarkus-version.yml +++ b/.github/workflows/build-for-quarkus-version.yml @@ -187,10 +187,11 @@ jobs: - name: Install Operator Lifecycle Manager and Operator SDK into Kind run: operator-sdk olm install --version v0.23.0 - - name: Validate OLM ExposedApp bundle +# Joke sample currently doesn't validate with OLM v1 because it bundles required Joke CRD, which v1 thinks should be owned + - name: Validate OLM bundles (excluding Joke sample) run: | - cd samples/exposedapp - operator-sdk bundle validate target/bundle/quarkus-operator-sdk-samples-exposedapp/ --select-optional suite=operatorframework + cd samples/ + find . -type d -name bundle | grep -v joke | xargs -I {} find {} -type d -maxdepth 1 -mindepth 1 | xargs -I {} operator-sdk bundle validate {} --select-optional suite=operatorframework - name: Run Joke sample using Quarkus DEV mode run: | diff --git a/annotations/src/main/java/io/quarkiverse/operatorsdk/annotations/CSVMetadata.java b/annotations/src/main/java/io/quarkiverse/operatorsdk/annotations/CSVMetadata.java index 0a4881ca8..32e690c1a 100644 --- a/annotations/src/main/java/io/quarkiverse/operatorsdk/annotations/CSVMetadata.java +++ b/annotations/src/main/java/io/quarkiverse/operatorsdk/annotations/CSVMetadata.java @@ -21,9 +21,29 @@ * gradle) name. This name can be used to assign reconcilers to the same bundle by creating a {@link SharedCSVMetadata} * implementation bearing a {@link CSVMetadata} annotation specifying the CSV metadata to be shared among reconcilers * assigned to that named bundle. + * + * @deprecated Use {@link #bundleName()} and {@link #csvName()} instead as previously this method was being used for both + * values resulting in confusion or even problems setting the correct CSV name as recommended by OLM. See + * this issue for a more detailed + * discussion. */ + @Deprecated String name() default ""; + /** + * The name which should be used for the generated bundle. If not provided, the name is derived from the project's (maven or + * gradle) name. This name can be used to assign reconcilers to the same bundle by creating a {@link SharedCSVMetadata} + * implementation bearing a {@link CSVMetadata} annotation specifying the CSV metadata to be shared among reconcilers + * assigned to that named bundle. + */ + String bundleName() default ""; + + /** + * The name used in the CSV metadata stanza. If not provided, this will default to {@code .v} + * as recommended by OLM. + */ + String csvName() default ""; + /** * Extra annotations that should be added to the CSV metadata. */ diff --git a/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/BundleGenerator.java b/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/BundleGenerator.java index a47931ef5..1c1b3077a 100644 --- a/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/BundleGenerator.java +++ b/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/BundleGenerator.java @@ -66,12 +66,12 @@ public static List prepareGeneration(BundleGenerationConfigura var missing = addCRDManifestBuilder(crds, builders, csvMetadata, csvBuilder.getOwnedCRs()); if (!missing.isEmpty()) { throw new IllegalStateException( - "Missing owned CRD data for resources: " + missing + " for bundle: " + csvMetadata.name); + "Missing owned CRD data for resources: " + missing + " for bundle: " + csvMetadata.bundleName); } // output required CRDs in the manifest, output a warning in case we're missing some missing = addCRDManifestBuilder(crds, builders, csvMetadata, csvBuilder.getRequiredCRs()); if (!missing.isEmpty()) { - log.warnv("Missing required CRD data for resources: {0} for bundle: {1}", missing, csvMetadata.name); + log.warnv("Missing required CRD data for resources: {0} for bundle: {1}", missing, csvMetadata.bundleName); } } @@ -93,7 +93,7 @@ private static HashSet addCRDManifestBuilder(Map crds, private static SortedMap generateBundleLabels(CSVMetadataHolder csvMetadata, BundleGenerationConfiguration bundleConfiguration, Version version) { - var packageName = bundleConfiguration.packageName.orElse(csvMetadata.name); + var packageName = bundleConfiguration.packageName.orElse(csvMetadata.bundleName); SortedMap values = new TreeMap<>(); values.put(join(BUNDLE_PREFIX, CHANNEL, DEFAULT, ANNOTATIONS_VERSION), 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 8f34ee896..ad57f0108 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 @@ -41,7 +41,9 @@ import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.GeneratedFileSystemResourceBuildItem; +import io.quarkus.deployment.pkg.builditem.JarBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.kubernetes.deployment.KubernetesCommonHelper; import io.quarkus.kubernetes.deployment.KubernetesConfig; import io.quarkus.kubernetes.deployment.ResourceNameUtil; @@ -51,6 +53,7 @@ public class BundleProcessor { private static final DotName SHARED_CSV_METADATA = DotName.createSimple(SharedCSVMetadata.class.getName()); 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"); public static final String CRD_DISPLAY_NAME = "CRD_DISPLAY_NAME"; public static final String CRD_DESCRIPTION = "CRD_DESCRIPTION"; @@ -69,7 +72,8 @@ public boolean getAsBoolean() { CSVMetadataBuildItem gatherCSVMetadata(KubernetesConfig kubernetesConfig, ApplicationInfoBuildItem appConfiguration, BundleGenerationConfiguration bundleConfiguration, - CombinedIndexBuildItem combinedIndexBuildItem) { + CombinedIndexBuildItem combinedIndexBuildItem, + JarBuildItem jarBuildItem) { final var index = combinedIndexBuildItem.getIndex(); final var defaultName = bundleConfiguration.packageName .orElse(ResourceNameUtil.getResourceName(kubernetesConfig, appConfiguration)); @@ -94,31 +98,37 @@ CSVMetadataBuildItem gatherCSVMetadata(KubernetesConfig kubernetesConfig, // Check whether the reconciler must be shipped using a custom bundle final var csvMetadataAnnotation = reconcilerInfo.classInfo() .declaredAnnotation(CSV_METADATA); - final var maybeCSVMetadataName = getCSVMetadataName(csvMetadataAnnotation); - final var csvMetadataName = maybeCSVMetadataName.orElse(defaultName); + final var sharedMetadataName = getBundleName(csvMetadataAnnotation, defaultName); + final var isNameInferred = defaultName.equals(sharedMetadataName); - var csvMetadata = sharedMetadataHolders.get(csvMetadataName); + var csvMetadata = sharedMetadataHolders.get(sharedMetadataName); if (csvMetadata == null) { final var origin = reconcilerInfo.classInfo().name().toString(); - if (!csvMetadataName.equals(defaultName)) { + if (!sharedMetadataName.equals(defaultName)) { final var maybeExistingOrigin = csvGroups.keySet().stream() - .filter(mh -> mh.name.equals(csvMetadataName)) + .filter(mh -> mh.bundleName.equals(sharedMetadataName)) .map(CSVMetadataHolder::getOrigin) .findFirst(); if (maybeExistingOrigin.isPresent()) { throw new IllegalStateException("Reconcilers '" + maybeExistingOrigin.get() + "' and '" + origin - + "' are using the same bundle name '" + csvMetadataName + + "' are using the same bundle name '" + sharedMetadataName + "' 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."); } } csvMetadata = createMetadataHolder(csvMetadataAnnotation, - new CSVMetadataHolder(csvMetadataName, defaultVersion, defaultReplaces, origin)); + new CSVMetadataHolder(sharedMetadataName, defaultVersion, defaultReplaces, + DEFAULT_PROVIDER_NAME, 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.", + origin, DEFAULT_PROVIDER_NAME); + } } log.infov("Assigning ''{0}'' reconciler to {1}", reconcilerInfo.nameOrFailIfUnset(), - getMetadataOriginInformation(csvMetadataAnnotation, maybeCSVMetadataName, csvMetadata)); + getMetadataOriginInformation(csvMetadataAnnotation, isNameInferred, csvMetadata)); csvGroups.computeIfAbsent(csvMetadata, m -> new ArrayList<>()).add( augmentReconcilerInfo(reconcilerInfo)); @@ -127,6 +137,37 @@ CSVMetadataBuildItem gatherCSVMetadata(KubernetesConfig kubernetesConfig, return new CSVMetadataBuildItem(csvGroups); } + private static String getDefaultProviderURLFromSCMInfo(ApplicationInfoBuildItem appConfiguration, + JarBuildItem jarBuildItem) { + final var maybeProject = KubernetesCommonHelper.createProject(appConfiguration, Optional.empty(), + jarBuildItem.getPath()); + return maybeProject.map(project -> { + final var scmInfo = project.getScmInfo(); + if (scmInfo != null) { + var origin = scmInfo.getRemote().get("origin"); + if (origin != null) { + try { + int atSign = origin.indexOf('@'); + if (atSign > 0) { + origin = origin.substring(atSign + 1); + origin = origin.replaceFirst(":", "/"); + origin = "https://" + origin; + } + + int dotGit = origin.indexOf(".git"); + if (dotGit > 0 && dotGit < origin.length() - 1) { + origin = origin.substring(0, dotGit); + } + return origin; + } catch (Exception e) { + log.warnv("Couldn't parse SCM information: {0}", origin); + } + } + } + return null; + }).orElse(null); + } + private static ReconcilerAugmentedClassInfo augmentReconcilerInfo( ReconcilerAugmentedClassInfo reconcilerInfo) { // if primary resource is a CR, check if it is annotated with CSVMetadata and augment it if it is @@ -161,14 +202,14 @@ private static void augmentResourceInfoIfCR(ReconciledAugmentedClassInfo reco } } - private String getMetadataOriginInformation(AnnotationInstance csvMetadataAnnotation, Optional csvMetadataName, + private String getMetadataOriginInformation(AnnotationInstance csvMetadataAnnotation, boolean isNameInferred, CSVMetadataHolder metadataHolder) { final var isDefault = csvMetadataAnnotation == null; - final var actualName = metadataHolder.name; + final var actualName = metadataHolder.bundleName; if (isDefault) { return "default bundle automatically named '" + actualName + "'"; } else { - return "bundle " + (csvMetadataName.isEmpty() ? "automatically " : "") + "named '" + return "bundle " + (isNameInferred ? "automatically " : "") + "named '" + actualName + "' defined by '" + metadataHolder.getOrigin() + "'"; } } @@ -256,7 +297,8 @@ void generateBundle(ApplicationInfoBuildItem configuration, private Map getSharedMetadataHolders(String name, String version, String defaultReplaces, IndexView index) { - CSVMetadataHolder csvMetadata = new CSVMetadataHolder(name, version, defaultReplaces, "default"); + CSVMetadataHolder csvMetadata = new CSVMetadataHolder(name, version, defaultReplaces, DEFAULT_PROVIDER_NAME, + "default"); final var sharedMetadataImpls = index.getAllKnownImplementors(SHARED_CSV_METADATA); final var result = new HashMap(sharedMetadataImpls.size() + 1); sharedMetadataImpls.forEach(sharedMetadataImpl -> { @@ -264,22 +306,31 @@ private Map getSharedMetadataHolders(String name, Str if (csvMetadataAnn != null) { final var origin = sharedMetadataImpl.name().toString(); final var metadataHolder = createMetadataHolder(csvMetadataAnn, csvMetadata, origin); - final var existing = result.get(metadataHolder.name); + final var existing = result.get(metadataHolder.bundleName); if (existing != null) { throw new IllegalStateException( - "Only one SharedCSVMetadata named " + metadataHolder.name + "Only one SharedCSVMetadata named " + metadataHolder.bundleName + " can be defined. Was defined on (at least): " + existing.getOrigin() + " and " + origin); } - result.put(metadataHolder.name, metadataHolder); + result.put(metadataHolder.bundleName, metadataHolder); } }); return result; } - private Optional getCSVMetadataName(AnnotationInstance csvMetadataAnnotation) { - return Optional.ofNullable(csvMetadataAnnotation) - .map(annotation -> annotation.value("name")) - .map(AnnotationValue::asString); + private static String getBundleName(AnnotationInstance csvMetadata, String defaultName) { + if (csvMetadata == null) { + return defaultName; + } else { + final var bundleName = csvMetadata.value("bundleName"); + if (bundleName != null) { + return bundleName.asString(); + } else { + return Optional.ofNullable(csvMetadata.value("name")) + .map(AnnotationValue::asString) + .orElse(defaultName); + } + } } private CSVMetadataHolder createMetadataHolder(AnnotationInstance csvMetadata, @@ -294,8 +345,8 @@ private CSVMetadataHolder createMetadataHolder(AnnotationInstance csvMetadata, C } final var providerField = csvMetadata.value("provider"); - String providerName = null; - String providerURL = null; + String providerName = mh.providerName; + String providerURL = mh.providerURL; if (providerField != null) { final var provider = providerField.asNested(); providerName = ConfigurationUtils.annotationValueOrDefault(provider, "name", @@ -441,8 +492,9 @@ private CSVMetadataHolder createMetadataHolder(AnnotationInstance csvMetadata, C } return new CSVMetadataHolder( - ConfigurationUtils.annotationValueOrDefault(csvMetadata, "name", - AnnotationValue::asString, () -> mh.name), + getBundleName(csvMetadata, mh.bundleName), + ConfigurationUtils.annotationValueOrDefault(csvMetadata, "csvName", + AnnotationValue::asString, () -> mh.csvName), ConfigurationUtils.annotationValueOrDefault(csvMetadata, "description", AnnotationValue::asString, () -> mh.description), ConfigurationUtils.annotationValueOrDefault(csvMetadata, "displayName", 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 1d2ab1fdd..b4e6656e3 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 @@ -54,7 +54,7 @@ public CsvManifestsBuilder(CSVMetadataHolder metadata, BuildTimeOperatorConfigur csvBuilder = new ClusterServiceVersionBuilder(); - final var metadataBuilder = csvBuilder.withNewMetadata().withName(getName()); + 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); @@ -72,7 +72,7 @@ public CsvManifestsBuilder(CSVMetadataHolder metadata, BuildTimeOperatorConfigur final var csvSpecBuilder = csvBuilder .editOrNewSpec() .withDescription(metadata.description) - .withDisplayName(defaultIfEmpty(metadata.displayName, getName())) + .withDisplayName(defaultIfEmpty(metadata.displayName, metadata.csvName)) .withKeywords(metadata.keywords) .withReplaces(metadata.replaces) .withVersion(metadata.version) diff --git a/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/builders/ManifestsBuilder.java b/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/builders/ManifestsBuilder.java index 4b8ec3aa7..fc9940fa8 100644 --- a/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/builders/ManifestsBuilder.java +++ b/bundle-generator/deployment/src/main/java/io/quarkiverse/operatorsdk/bundle/deployment/builders/ManifestsBuilder.java @@ -45,7 +45,7 @@ public abstract byte[] getManifestData(List serviceAccounts, Lis public abstract String getManifestType(); public String getName() { - return metadata.name; + return metadata.bundleName; } @Override diff --git a/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/DefaultBundleWhenNoCsvMetadataTest.java b/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/DefaultBundleWhenNoCsvMetadataTest.java index 3183dc188..21a6296df 100644 --- a/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/DefaultBundleWhenNoCsvMetadataTest.java +++ b/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/DefaultBundleWhenNoCsvMetadataTest.java @@ -37,6 +37,7 @@ public void shouldWriteBundleEvenWhenCsvMetadataIsNotUsed() throws IOException { assertEquals(name, deployment.getName()); // by default, we shouldn't output the version label in the selector match labels as the default controlling this should be overridden by KubernetesLabelConfigOverrider assertNull(deployment.getSpec().getSelector().getMatchLabels().get("app.kubernetes.io/version")); + assertEquals(System.getProperty("user.name"), csv.getSpec().getProvider().getName()); } } 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 8ee539ddc..fdd385bd9 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 @@ -3,6 +3,7 @@ import java.util.List; import java.util.Optional; +import io.quarkiverse.operatorsdk.annotations.CSVMetadata; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -28,7 +29,10 @@ public class BundleGenerationConfiguration { /** * The name of the package that bundle belongs to. + * + * @deprecated Use {@link CSVMetadata#bundleName()} instead */ + @Deprecated(forRemoval = true) @ConfigItem public Optional packageName; 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 a140c6f54..ed4e4d454 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 @@ -5,7 +5,8 @@ import java.util.Objects; public class CSVMetadataHolder { - public final String name; + public final String bundleName; + public final String csvName; private final String origin; public final String description; public final String displayName; @@ -117,27 +118,32 @@ public RequiredCRD(String kind, String name, String version) { } - public CSVMetadataHolder(String name, String version, String replaces, String origin) { - this(name, null, null, null, null, null, null, replaces, null, version, null, null, null, null, null, null, null, null, + public CSVMetadataHolder(String bundleName, String version, String replaces, String providerName, String origin) { + this(bundleName, null, null, null, null, null, providerName, null, replaces, null, version, null, null, null, + null, null, + null, null, + null, origin); } - public CSVMetadataHolder(String name, String description, String displayName, Annotations annotations, String[] keywords, + public CSVMetadataHolder(String bundleName, String csvName, String description, String displayName, Annotations annotations, + String[] keywords, String providerName, String providerURL, String replaces, String[] skips, String version, String maturity, String minKubeVersion, Maintainer[] maintainers, Link[] links, Icon[] icon, InstallMode[] installModes, PermissionRule[] permissionRules, RequiredCRD[] requiredCRDs, String origin) { - this.name = name; + this.bundleName = bundleName; + assert version != null; + this.version = version; + this.csvName = csvName == null ? bundleName + ".v" + version.toLowerCase() : csvName; this.description = description; this.displayName = displayName; this.annotations = annotations; this.keywords = keywords; - this.providerName = providerName; this.providerURL = providerURL; this.replaces = replaces; this.skips = skips; - this.version = version; this.maturity = maturity; this.minKubeVersion = minKubeVersion; this.maintainers = maintainers; @@ -147,8 +153,13 @@ public CSVMetadataHolder(String name, String description, String displayName, An this.permissionRules = permissionRules; this.requiredCRDs = requiredCRDs; this.origin = origin; + this.providerName = providerName; } + /** + * CSVMetadataHolders are stored as key in Maps and identified by their associated bundle name, so that's what we use for + * equals and {@link #hashCode()} + */ @Override public boolean equals(Object o) { if (this == o) @@ -156,12 +167,12 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; CSVMetadataHolder that = (CSVMetadataHolder) o; - return Objects.equals(name, that.name); + return Objects.equals(bundleName, that.bundleName); } @Override public int hashCode() { - return Objects.hash(name); + return Objects.hash(bundleName); } public String getOrigin() { diff --git a/samples/exposedapp/src/main/java/io/halkyon/ExposedAppReconciler.java b/samples/exposedapp/src/main/java/io/halkyon/ExposedAppReconciler.java index 26199faff..5476ad7a6 100644 --- a/samples/exposedapp/src/main/java/io/halkyon/ExposedAppReconciler.java +++ b/samples/exposedapp/src/main/java/io/halkyon/ExposedAppReconciler.java @@ -20,7 +20,7 @@ @Dependent(name = "service", type = ServiceDependent.class), @Dependent(type = IngressDependent.class, readyPostcondition = IngressDependent.class) }) -@CSVMetadata(displayName = "ExposedApp operator", description = "A sample operator that shows how to use JOSDK's main features with the Quarkus extension", provider = @CSVMetadata.Provider(name = "Christophe Laprun", url = "https://github.com/metacosm")) +@CSVMetadata(displayName = "ExposedApp operator", description = "A sample operator that shows how to use JOSDK's main features with the Quarkus extension") public class ExposedAppReconciler implements Reconciler, ContextInitializer {