Skip to content

Commit

Permalink
Merge pull request quarkusio#43512 from aloubyansky/platform-wo-quark…
Browse files Browse the repository at this point in the history
…us-bom

Fixes issues with platforms w/o quarkus-bom and extension catalog metadata map merging
  • Loading branch information
aloubyansky authored Sep 27, 2024
2 parents 10ff88f + decd977 commit 7668da3
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 55 deletions.
47 changes: 47 additions & 0 deletions docs/src/main/asciidoc/extension-codestart.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,53 @@ It is going to be merged with the other codestarts config and automatically conv
* You can start with java and add kotlin later in another PR (create an issue so you don't forget).
* If you have a question, ping me @ia3andy on https://quarkusio.zulipchat.com/.

=== Platform codestarts data

This chapter is relevant for https://quarkus.io/guides/platform[Quarkus platform] developers who want to provide codestart data in the https://quarkus.io/guides/platform#platform-descriptor[platform metadata].

While typically codestart data is configured in <<codestart-yml>> files, platform developers may also provide codestart data in a platform descriptor with the purpose of customizing certain values.

For example, given a `codestart.yml` such as
[source,yaml]
----
name: quarkus-magic-codestart
ref: quarkus-magic
type: code
tags: extension-codestart
metadata:
title: Quarkus Magic
description: Quarkus magic
language:
base:
data:
magic:
source: codestart.yml
----

The value of `magic.source` can be customized in a platform descriptor like this
[source,yaml]
----
{
"id" : "org.acme.platform:acme-bom-quarkus-platform-descriptor:7.0.7:json:7.0.7",
"platform" : true,
"bom" : "org.acme.platform:acme-magic-bom::pom:7.0.7",
"metadata" : {
"project" : { <1>
"codestart-data" : { <2>
"quarkus-magic-codestart" : { <3>
"magic" : {
"source" : "acme-platform"
}
}
}
},
...
----

<1> `project` groups metadata that is relevant for project creation tools
<2> `codestart-data` is a source of data for various codestarts
<3> a name of the codestart to which the data nested under it should be passed

== The generator sources

* https://github.com/quarkusio/quarkus/tree/main/independent-projects/tools/codestarts[Codestart generator, window="_blank"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -47,7 +45,7 @@ public class CreateProjectCommandHandler implements QuarkusCommandHandler {

@Override
public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws QuarkusCommandException {
final Set<String> extensionsQuery = invocation.getValue(EXTENSIONS, Collections.emptySet());
final Set<String> extensionsQuery = invocation.getValue(EXTENSIONS, Set.of());

// Default to cleaned groupId if packageName not set
final String className = invocation.getStringValue(RESOURCE_CLASS_NAME);
Expand All @@ -72,10 +70,13 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws
final List<ExtensionCatalog> extensionOrigins = getExtensionOrigins(mainCatalog, extensionsToAdd);

final List<ArtifactCoords> platformBoms = new ArrayList<>(Math.max(extensionOrigins.size(), 1));
if (extensionOrigins.size() > 0) {
Map<String, Object> platformProjectData;
if (!extensionOrigins.isEmpty()) {
// necessary to set the versions from the selected origins
extensionsToAdd = computeRequiredExtensions(CatalogMergeUtility.merge(extensionOrigins), extensionsQuery,
invocation.log());
final ExtensionCatalog mergedCatalog = CatalogMergeUtility.merge(extensionOrigins);
platformProjectData = ToolsUtils.readProjectData(mergedCatalog);
setQuarkusProperties(invocation, mergedCatalog);
extensionsToAdd = computeRequiredExtensions(mergedCatalog, extensionsQuery, invocation.log());
// collect platform BOMs to import
boolean sawFirstPlatform = false;
for (ExtensionCatalog c : extensionOrigins) {
Expand All @@ -89,7 +90,9 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws
platformBoms.add(c.getBom());
}
} else {
platformProjectData = ToolsUtils.readProjectData(mainCatalog);
platformBoms.add(mainCatalog.getBom());
setQuarkusProperties(invocation, mainCatalog);
}

final List<ArtifactCoords> extensionCoords = new ArrayList<>(extensionsToAdd.size());
Expand All @@ -108,13 +111,6 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws
invocation.setValue(CatalogKey.BOM_ARTIFACT_ID, mainCatalog.getBom().getArtifactId());
invocation.setValue(CatalogKey.BOM_VERSION, mainCatalog.getBom().getVersion());
invocation.setValue(QUARKUS_VERSION, mainCatalog.getQuarkusCoreVersion());
final Properties quarkusProps = ToolsUtils.readQuarkusProperties(mainCatalog);
quarkusProps.forEach((k, v) -> {
final String name = k.toString();
if (!invocation.hasValue(name)) {
invocation.setValue(name, v.toString());
}
});

try {
Map<String, Object> platformData = new HashMap<>();
Expand All @@ -130,15 +126,17 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws
.buildTool(invocation.getQuarkusProject().getBuildTool())
.example(invocation.getValue(EXAMPLE))
.noCode(invocation.getValue(NO_CODE, false))
.addCodestarts(invocation.getValue(EXTRA_CODESTARTS, Collections.emptySet()))
.addCodestarts(invocation.getValue(EXTRA_CODESTARTS, Set.of()))
.noBuildToolWrapper(invocation.getValue(NO_BUILDTOOL_WRAPPER, false))
.noDockerfiles(invocation.getValue(NO_DOCKERFILES, false))
.addData(platformProjectData)
.addData(platformData)
.addData(toCodestartData(invocation.getValues()))
.addData(invocation.getValue(DATA, Collections.emptyMap()))
.addData(invocation.getValue(DATA, Map.of()))
.messageWriter(invocation.log())
.defaultCodestart(getDefaultCodestart(mainCatalog))
.build();

invocation.log().info("-----------");
if (!extensionsToAdd.isEmpty()) {
invocation.log().info("selected extensions: \n"
Expand All @@ -165,6 +163,16 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws
return QuarkusCommandOutcome.success();
}

private static void setQuarkusProperties(QuarkusCommandInvocation invocation, ExtensionCatalog catalog) {
var quarkusProps = ToolsUtils.readQuarkusProperties(catalog);
quarkusProps.forEach((k, v) -> {
final String name = k.toString();
if (!invocation.hasValue(name)) {
invocation.setValue(name, v.toString());
}
});
}

private List<Extension> computeRequiredExtensions(ExtensionCatalog catalog,
final Set<String> extensionsQuery, MessageWriter log) throws QuarkusCommandException {
final List<Extension> extensionsToAdd = computeExtensionsFromQuery(catalog, extensionsQuery, log);
Expand All @@ -178,25 +186,54 @@ private static List<ExtensionCatalog> getExtensionOrigins(ExtensionCatalog exten
List<Extension> extensionsToAdd)
throws QuarkusCommandException {

final List<ExtensionOrigins> extOrigins = new ArrayList<>(extensionsToAdd.size());
for (Extension e : extensionsToAdd) {
addOrigins(extOrigins, e);
}

if (extOrigins.isEmpty()) {
// legacy 1.x or universe platform
return Collections.emptyList();
if (extensionsToAdd.isEmpty() && extensionCatalog.isPlatform()) {
return List.of(extensionCatalog);
}

// we add quarkus-core as a selected extension here only to include the quarkus-bom
// in the list of platforms. quarkus-core won't be added to the generated POM though.
final Optional<Extension> quarkusCore = extensionCatalog.getExtensions().stream()
.filter(e -> e.getArtifact().getArtifactId().equals("quarkus-core")).findFirst();
if (!quarkusCore.isPresent()) {
throw new QuarkusCommandException("Failed to locate quarkus-core in the extension catalog");
final Extension quarkusCore = findQuarkusCore(extensionCatalog);
final List<ExtensionOrigins> originsWithPreferences;
if (extensionsToAdd.isEmpty()) {
// if no extensions were requested, we select the core BOM
if (quarkusCore.getOrigins().size() == 1 && quarkusCore.getOrigins().get(0) instanceof ExtensionCatalog) {
// in this case, there is only one origin to choose from
return List.of((ExtensionCatalog) quarkusCore.getOrigins().get(0));
}
originsWithPreferences = new ArrayList<>(quarkusCore.getOrigins().size());
extensionsToAdd = List.of(quarkusCore);
} else {
originsWithPreferences = new ArrayList<>();
for (Extension e : extensionsToAdd) {
addOriginsWithPreferences(originsWithPreferences, e);
}
}
addOrigins(extOrigins, quarkusCore.get());

addOriginsWithPreferences(originsWithPreferences, quarkusCore);
if (!originsWithPreferences.isEmpty()) {
return getRecommendedOrigins(extensionsToAdd, originsWithPreferences);
}

// no origin preferences were found (origin-preference data is missing)
// this will happen if the extension catalog wasn't provided by a registry client
if (extensionCatalog.isPlatform()) {
// if the original catalog is platform, it should cover all the requested extensions
return List.of(extensionCatalog);
}

// fallback to the best guess
final Map<String, ExtensionCatalog> catalogMap = new HashMap<>();
for (var e : extensionsToAdd) {
var origin = e.getOrigins().get(0);
if (origin instanceof ExtensionCatalog) {
catalogMap.putIfAbsent(origin.getId(), (ExtensionCatalog) origin);
}
}
return List.copyOf(catalogMap.values());
}

private static List<ExtensionCatalog> getRecommendedOrigins(List<Extension> extensionsToAdd,
List<ExtensionOrigins> extOrigins) throws QuarkusCommandException {
final OriginCombination recommendedCombination = OriginSelector.of(extOrigins).calculateRecommendedCombination();
if (recommendedCombination == null) {
final StringBuilder buf = new StringBuilder();
Expand All @@ -207,24 +244,30 @@ private static List<ExtensionCatalog> getExtensionOrigins(ExtensionCatalog exten
}
throw new QuarkusCommandException(buf.toString());
}
return recommendedCombination.getUniqueSortedOrigins().stream().map(o -> o.getCatalog()).collect(Collectors.toList());
return recommendedCombination.getUniqueSortedCatalogs();
}

private static void addOrigins(final List<ExtensionOrigins> extOrigins, Extension e) {
private static Extension findQuarkusCore(ExtensionCatalog extensionCatalog) throws QuarkusCommandException {
final Optional<Extension> quarkusCore = extensionCatalog.getExtensions().stream()
.filter(e -> e.getArtifact().getArtifactId().equals("quarkus-core")).findFirst();
if (quarkusCore.isEmpty()) {
throw new QuarkusCommandException("Failed to locate quarkus-core in the extension catalog");
}
return quarkusCore.get();
}

private static void addOriginsWithPreferences(final List<ExtensionOrigins> extOrigins, Extension e) {
ExtensionOrigins.Builder eoBuilder = null;
for (ExtensionOrigin o : e.getOrigins()) {
if (!(o instanceof ExtensionCatalog)) {
continue;
}
final ExtensionCatalog c = (ExtensionCatalog) o;
final OriginPreference op = (OriginPreference) c.getMetadata().get("origin-preference");
if (op == null) {
continue;
}
if (eoBuilder == null) {
eoBuilder = ExtensionOrigins.builder(e.getArtifact().getKey());
for (ExtensionOrigin c : e.getOrigins()) {
if (c instanceof ExtensionCatalog) {
final OriginPreference op = (OriginPreference) c.getMetadata().get("origin-preference");
if (op != null) {
if (eoBuilder == null) {
eoBuilder = ExtensionOrigins.builder(e.getArtifact().getKey());
}
eoBuilder.addOrigin((ExtensionCatalog) c, op);
}
}
eoBuilder.addOrigin(c, op);
}
if (eoBuilder != null) {
extOrigins.add(eoBuilder.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import io.quarkus.devtools.commands.data.QuarkusCommandException;
import io.quarkus.devtools.messagewriter.MessageWriter;
Expand Down Expand Up @@ -137,7 +136,7 @@ public static ProjectState resolveRecommendedState(ProjectState currentState, Ex

final List<ExtensionCatalog> recommendedOrigins;
try {
recommendedOrigins = getRecommendedOrigins(recommendedCatalog, updateCandidates);
recommendedOrigins = getRecommendedOrigins(updateCandidates);
} catch (QuarkusCommandException e) {
log.warn("Failed to find a compatible configuration update for the project");
return currentState;
Expand Down Expand Up @@ -192,7 +191,7 @@ public static ProjectState resolveRecommendedState(ProjectState currentState, Ex
return stateBuilder.build();
}

private static List<ExtensionCatalog> getRecommendedOrigins(ExtensionCatalog extensionCatalog, List<Extension> extensions)
private static List<ExtensionCatalog> getRecommendedOrigins(List<Extension> extensions)
throws QuarkusCommandException {
final List<ExtensionOrigins> extOrigins = new ArrayList<>(extensions.size());
for (Extension e : extensions) {
Expand All @@ -209,7 +208,7 @@ private static List<ExtensionCatalog> getRecommendedOrigins(ExtensionCatalog ext
}
throw new QuarkusCommandException(buf.toString());
}
return recommendedCombination.getUniqueSortedOrigins().stream().map(o -> o.getCatalog()).collect(Collectors.toList());
return recommendedCombination.getUniqueSortedCatalogs();
}

private static void addOrigins(final List<ExtensionOrigins> extOrigins, Extension e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ public static Properties readQuarkusProperties(ExtensionCatalog catalog) {
return properties;
}

@SuppressWarnings("unchecked")
public static Map<String, Object> readProjectData(ExtensionCatalog catalog) {
Map<Object, Object> map = (Map<Object, Object>) catalog.getMetadata().getOrDefault("project", Map.of());
return (Map<String, Object>) map.getOrDefault("codestart-data", Map.of());
}

public static String requireProperty(Properties props, String name) {
final String value = props.getProperty(name);
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public void build() {
throw new IllegalStateException("Failed to serialize registry client configuration " + configYaml, e);
}

externalCodestartBuilders.forEach(b -> b.persist());
externalCodestartBuilders.forEach(TestCodestartBuilder::persist);
installExtensionArtifacts(externalExtensions);
}

Expand Down Expand Up @@ -673,6 +673,7 @@ public static class TestPlatformCatalogMemberBuilder {

private final TestPlatformCatalogReleaseBuilder release;
private final ExtensionCatalog.Mutable extensions = ExtensionCatalog.builder();
private Map<ArtifactCoords, Path> codestartArtifacts;
private final Model pom;

private TestPlatformCatalogMemberBuilder(TestPlatformCatalogReleaseBuilder release, ArtifactCoords bom) {
Expand Down Expand Up @@ -740,11 +741,24 @@ public TestPlatformCatalogMemberBuilder addExtension(String artifactId) {
}

public TestPlatformCatalogMemberBuilder addExtension(String groupId, String artifactId, String version) {
return addExtensionWithCodestart(groupId, artifactId, version, null);
}

public TestPlatformCatalogMemberBuilder addExtensionWithCodestart(String artifactId, String codestart) {
return addExtensionWithCodestart(extensions.getBom().getGroupId(), artifactId, extensions.getBom().getVersion(),
codestart);
}

public TestPlatformCatalogMemberBuilder addExtensionWithCodestart(String groupId, String artifactId, String version,
String codestart) {
final ArtifactCoords coords = ArtifactCoords.jar(groupId, artifactId, version);
final Extension.Mutable e = Extension.builder()
.setArtifact(coords)
.setName(artifactId)
.setOrigins(Collections.singletonList(extensions));
if (codestart != null) {
e.getMetadata().put("codestart", Map.of("name", codestart, "languages", List.of("java")));
}
extensions.addExtension(e);

final Dependency d = new Dependency();
Expand All @@ -761,6 +775,22 @@ public TestPlatformCatalogMemberBuilder addExtension(String groupId, String arti
return this;
}

public TestPlatformCatalogMemberBuilder addCodestartsArtifact(ArtifactCoords coords, Path jarFile) {
if (codestartArtifacts == null) {
codestartArtifacts = new LinkedHashMap<>();
}
codestartArtifacts.put(coords, jarFile);
return this;
}

@SuppressWarnings("unchecked")
public TestPlatformCatalogMemberBuilder setPlatformProjectCodestartData(String name, Object value) {
var map = (Map<String, Object>) extensions.getMetadata().computeIfAbsent("project", k -> new HashMap<>());
map = (Map<String, Object>) map.computeIfAbsent("codestart-data", k -> new HashMap<>());
map.put(name, value);
return this;
}

public TestPlatformCatalogReleaseBuilder release() {
return release;
}
Expand All @@ -781,6 +811,16 @@ private void install(ArtifactCoords coords, Path path) {
}

private void persist(Path memberDir) {

if (codestartArtifacts != null) {
final List<String> artifacts = new ArrayList<>(codestartArtifacts.size());
for (var entry : codestartArtifacts.entrySet()) {
artifacts.add(entry.getKey().toGACTVString());
install(entry.getKey(), entry.getValue());
}
extensions.getMetadata().put("codestarts-artifacts", artifacts);
}

release.setReleaseInfo(extensions);
final ArtifactCoords bom = extensions.getBom();
final Path json = getMemberCatalogPath(memberDir, bom);
Expand Down
Loading

0 comments on commit 7668da3

Please sign in to comment.