diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 675ee5711d55d..37edf36bfee9c 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -20,11 +20,6 @@ 1.0.16 5.0.0 3.0.2 - 3.0.0 - 2.1.0 - 5.0.1 - 2.1.2 - 3.1.0 3.1.2 1.3.2 1 @@ -57,7 +52,7 @@ 3.1.1 2.1.0 3.3.3 - 4.0.2 + 4.0.4 4.0.0 3.5.0 2.3.0 @@ -72,16 +67,22 @@ 2.3.1 2.1.2 2.1.1 + 3.0.0 + 2.1.0 + 5.0.1 4.0.1 2.0.1 2.1.0 + 2.1.2 3.0.0 2.0.1 3.1.0 + 2.1.0 6.0.0 2.0.1 3.0.2 2.1.1 + 3.1.0 4.0.0 4.0.3 9.5 @@ -125,7 +126,7 @@ 4.1.5 9.2.1 2.3.2 - 2.1.214 + 2.2.220 42.6.0 3.1.4 8.0.30 @@ -138,7 +139,7 @@ 5.3.0 5.9.3 1.5.0 - 14.0.11.Final + 14.0.13.Final 4.6.2.Final 3.1.5 4.1.94.Final @@ -4574,6 +4575,11 @@ jakarta.persistence-api ${jakarta.persistence-api.version} + + jakarta.resource + jakarta.resource-api + ${jakarta.resource-api.version} + jakarta.servlet jakarta.servlet-api diff --git a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java index bcc15ea7ec38f..b986340941039 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java @@ -24,6 +24,7 @@ import io.quarkus.bootstrap.prebuild.CodeGenException; import io.quarkus.deployment.codegen.CodeGenData; import io.quarkus.deployment.configuration.BuildTimeConfigurationReader; +import io.quarkus.deployment.configuration.tracker.ConfigTrackingValueTransformer; import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.DevModeContext.ModuleInfo; import io.quarkus.maven.dependency.ResolvedDependency; @@ -185,6 +186,43 @@ public static boolean trigger(ClassLoader deploymentClassLoader, }); } + /** + * Initializes an application build time configuration and returns current values of properties + * passed in as {@code originalProperties}. + * + * @param appModel application model + * @param launchMode launch mode + * @param buildSystemProps build system (or project) properties + * @param deploymentClassLoader build classloader + * @param originalProperties properties to read from the initialized configuration + * @return current values of the passed in original properties + */ + public static Properties readCurrentConfigValues(ApplicationModel appModel, String launchMode, + Properties buildSystemProps, + QuarkusClassLoader deploymentClassLoader, Properties originalProperties) { + Config config = null; + try { + config = getConfig(appModel, LaunchMode.valueOf(launchMode), buildSystemProps, deploymentClassLoader); + } catch (CodeGenException e) { + throw new RuntimeException("Failed to load application configuration", e); + } + var valueTransformer = ConfigTrackingValueTransformer.newInstance(config); + final Properties currentValues = new Properties(originalProperties.size()); + for (var originalProp : originalProperties.entrySet()) { + var name = originalProp.getKey().toString(); + var currentValue = config.getConfigValue(name); + final String current = valueTransformer.transform(name, currentValue); + if (!originalProp.getValue().equals(current)) { + log.info("Option " + name + " has changed since the last build from " + + originalProp.getValue() + " to " + current); + } + if (current != null) { + currentValues.put(name, current); + } + } + return currentValues; + } + public static Config getConfig(ApplicationModel appModel, LaunchMode launchMode, Properties buildSystemProps, QuarkusClassLoader deploymentClassLoader) throws CodeGenException { final Map> unavailableConfigServices = getUnavailableConfigServices(appModel.getAppArtifact(), diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index 645efa33e996e..fd33610c90ca0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -47,6 +47,7 @@ import io.quarkus.deployment.configuration.matching.FieldContainer; import io.quarkus.deployment.configuration.matching.MapContainer; import io.quarkus.deployment.configuration.matching.PatternMapBuilder; +import io.quarkus.deployment.configuration.tracker.ConfigTrackingInterceptor; import io.quarkus.deployment.configuration.type.ArrayOf; import io.quarkus.deployment.configuration.type.CollectionOf; import io.quarkus.deployment.configuration.type.ConverterType; @@ -122,6 +123,8 @@ private static List> collectConfigRoots(ClassLoader classLoader) throws final Set deprecatedProperties; final Set deprecatedRuntimeProperties; + final ConfigTrackingInterceptor buildConfigTracker; + /** * Initializes a new instance with located configuration root classes on the classpath * of a given classloader. @@ -233,6 +236,8 @@ private BuildTimeConfigurationReader(ClassLoader classLoader, final List clazz, @@ -399,11 +404,15 @@ public SmallRyeConfig initConfiguration(LaunchMode launchMode, Properties buildS for (ConfigClassWithPrefix mapping : getBuildTimeVisibleMappings()) { builder.withMapping(mapping.getKlass(), mapping.getPrefix()); } - return builder.build(); + + builder.withInterceptors(buildConfigTracker); + var config = builder.build(); + buildConfigTracker.configure(config); + return config; } public ReadResult readConfiguration(final SmallRyeConfig config) { - return SecretKeys.doUnlocked(() -> new ReadOperation(config).run()); + return SecretKeys.doUnlocked(() -> new ReadOperation(config, buildConfigTracker).run()); } private Set getDeprecatedProperties(Iterable rootDefinitions) { @@ -459,6 +468,7 @@ private void collectDeprecatedConfigItems(ClassMember classMember, Set d final class ReadOperation { final SmallRyeConfig config; + final ConfigTrackingInterceptor buildConfigTracker; final Set processedNames = new HashSet<>(); final Map, Object> objectsByClass = new HashMap<>(); @@ -468,8 +478,9 @@ final class ReadOperation { final Map> convByType = new HashMap<>(); - ReadOperation(final SmallRyeConfig config) { + ReadOperation(final SmallRyeConfig config, ConfigTrackingInterceptor buildConfigTracker) { this.config = config; + this.buildConfigTracker = buildConfigTracker; } ReadResult run() { @@ -662,6 +673,7 @@ ReadResult run() { .setRunTimeMappings(runTimeMappings) .setUnknownBuildProperties(unknownBuildProperties) .setDeprecatedRuntimeProperties(deprecatedRuntimeProperties) + .setBuildConfigTracker(buildConfigTracker) .createReadResult(); } @@ -1129,6 +1141,7 @@ public static final class ReadResult { final Set unknownBuildProperties; final Set deprecatedRuntimeProperties; + final ConfigTrackingInterceptor.ReadOptionsProvider readOptionsProvider; public ReadResult(final Builder builder) { this.objectsByClass = builder.getObjectsByClass(); @@ -1151,6 +1164,8 @@ public ReadResult(final Builder builder) { this.unknownBuildProperties = builder.getUnknownBuildProperties(); this.deprecatedRuntimeProperties = builder.deprecatedRuntimeProperties; + this.readOptionsProvider = builder.buildConfigTracker == null ? null + : builder.buildConfigTracker.getReadOptionsProvider(); } private static Map, RootDefinition> rootsToMap(Builder builder) { @@ -1243,6 +1258,10 @@ public Object requireObjectForClass(Class clazz) { return obj; } + public ConfigTrackingInterceptor.ReadOptionsProvider getReadOptionsProvider() { + return readOptionsProvider; + } + static class Builder { private Map, Object> objectsByClass; private Map allBuildTimeValues; @@ -1257,6 +1276,7 @@ static class Builder { private List runTimeMappings; private Set unknownBuildProperties; private Set deprecatedRuntimeProperties; + private ConfigTrackingInterceptor buildConfigTracker; Map, Object> getObjectsByClass() { return objectsByClass; @@ -1371,6 +1391,11 @@ Builder setDeprecatedRuntimeProperties(Set deprecatedRuntimeProperties) return this; } + Builder setBuildConfigTracker(ConfigTrackingInterceptor buildConfigTracker) { + this.buildConfigTracker = buildConfigTracker; + return this; + } + ReadResult createReadResult() { return new ReadResult(this); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingConfig.java new file mode 100644 index 0000000000000..6508e33023b89 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingConfig.java @@ -0,0 +1,103 @@ +package io.quarkus.deployment.configuration.tracker; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.util.GlobUtil; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +/** + * Configuration options for application build time configuration usage tracking + * and dumping. + */ +@ConfigMapping(prefix = "quarkus.config-tracking") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface ConfigTrackingConfig { + + /** + * Whether configuration dumping is enabled + */ + @WithDefault("false") + boolean enabled(); + + /** + * Directory in which the configuration dump should be stored. + * If not configured the {@code .quarkus} directory under the project directory will be used. + */ + Optional directory(); + + /** + * File in which the configuration dump should be stored. If not configured, the {@link #filePrefix} and + * {@link #fileSuffix} will be used to generate the final file name. + * If the configured file path is absolute, the {@link #directory} option will be ignored. Otherwise, + * the path will be considered relative to the {@link #directory}. + */ + Optional file(); + + /** + * File name prefix. This option will be ignored in case {@link #file} is configured. + */ + @WithDefault("quarkus") + String filePrefix(); + + /** + * File name suffix. This option will be ignored in case {@link #file} is configured. + */ + @WithDefault("-config-dump") + String fileSuffix(); + + /** + * A list of config properties that should be excluded from the report. + * GLOB patterns could be used instead of property names. + */ + Optional> exclude(); + + /** + * Translates the value of {@link #exclude} to a list of {@link java.util.regex.Pattern}. + * + * @return list of patterns created from {@link #exclude} + */ + default List getExcludePatterns() { + return toPatterns(exclude()); + } + + /** + * A list of config properties whose values should be hashed in the report. + * The values will be hashed using SHA-512 algorithm. + * GLOB patterns could be used instead of property names. + */ + Optional> hashOptions(); + + /** + * Translates the value of {@link #hashOptions()} to a list of {@link java.util.regex.Pattern}. + * + * @return list of patterns created from {@link #hashOptions()} + */ + default List getHashOptionsPatterns() { + return toPatterns(hashOptions()); + } + + static List toPatterns(Optional> globs) { + if (globs.isEmpty()) { + return List.of(); + } + var list = globs.get(); + final List patterns = new ArrayList<>(list.size()); + for (var s : list) { + patterns.add(Pattern.compile(GlobUtil.toRegexPattern(s))); + } + return patterns; + } + + /** + * Whether to use a {@code ~} as an alias for user home directory in path values + */ + @WithDefault("true") + boolean useUserHomeAliasInPaths(); +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingInterceptor.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingInterceptor.java new file mode 100644 index 0000000000000..3e5f92866492a --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingInterceptor.java @@ -0,0 +1,89 @@ +package io.quarkus.deployment.configuration.tracker; + +import static io.smallrye.config.SecretKeys.doLocked; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.annotation.Priority; + +import org.eclipse.microprofile.config.Config; + +import io.quarkus.deployment.configuration.BuildTimeConfigurationReader; +import io.quarkus.runtime.LaunchMode; +import io.smallrye.config.ConfigSourceInterceptor; +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.Priorities; + +/** + * Build configuration interceptor that records all the configuration options + * and their values that are read during the build. + */ +@Priority(Priorities.APPLICATION) +public class ConfigTrackingInterceptor implements ConfigSourceInterceptor { + + /** + * A writer that persists collected configuration options and their values to a file + */ + public interface ConfigurationWriter { + void write(ConfigTrackingConfig config, BuildTimeConfigurationReader.ReadResult configReadResult, + LaunchMode launchMode, Path buildDirectory); + } + + /** + * Provides an immutable map of options that were read during the build. + */ + public interface ReadOptionsProvider { + + /** + * An immutable map of options read during the build. + * + * @return immutable map of options read during the build + */ + Map getReadOptions(); + } + + private boolean enabled; + // it's a String value map to be able to represent null (not configured) values + private Map readOptions = Map.of(); + private final ReadOptionsProvider readOptionsProvider = new ReadOptionsProvider() { + @Override + public Map getReadOptions() { + return Collections.unmodifiableMap(readOptions); + } + }; + + /** + * Initializes the configuration tracker + * + * @param config configuration instance + */ + public void configure(Config config) { + enabled = config.getValue("quarkus.config-tracking.enabled", boolean.class); + if (enabled) { + readOptions = new ConcurrentHashMap<>(); + } + } + + @Override + public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { + if (!enabled) { + return context.proceed(name); + } + final ConfigValue configValue = doLocked(() -> context.proceed(name)); + readOptions.put(name, ConfigTrackingValueTransformer.asString(configValue)); + return configValue; + } + + /** + * Read options orvipder. + * + * @return read options provider + */ + public ReadOptionsProvider getReadOptionsProvider() { + return readOptionsProvider; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingValueTransformer.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingValueTransformer.java new file mode 100644 index 0000000000000..7c098a6b722cf --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingValueTransformer.java @@ -0,0 +1,129 @@ +package io.quarkus.deployment.configuration.tracker; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.StringJoiner; +import java.util.regex.Pattern; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigValue; + +import io.quarkus.bootstrap.util.PropertyUtils; +import io.smallrye.config.SmallRyeConfig; + +/** + * Transforms configuration values before they are written to a file + */ +public class ConfigTrackingValueTransformer { + + private static final String NOT_CONFIGURED = "quarkus.config-tracking:not-configured"; + private static final String PATH_ELEMENT_SEPARATOR = "/"; + private static final String USER_HOME_DIR_ALIAS = "~"; + + private static volatile MessageDigest SHA512; + + private static MessageDigest getSHA512() { + if (SHA512 == null) { + try { + SHA512 = MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + return SHA512; + } + + public static ConfigTrackingValueTransformer newInstance(Config config) { + return new ConfigTrackingValueTransformer( + config.unwrap(SmallRyeConfig.class).getConfigMapping(ConfigTrackingConfig.class)); + } + + public static ConfigTrackingValueTransformer newInstance(ConfigTrackingConfig config) { + return new ConfigTrackingValueTransformer(config); + } + + /** + * Returns a non-null string value for a given {@link org.eclipse.microprofile.config.ConfigValue} instance. + * + * @param value configuration value + * @return non-null string value for a given {@link org.eclipse.microprofile.config.ConfigValue} instance + */ + public static String asString(ConfigValue value) { + return value == null ? NOT_CONFIGURED : value.getValue(); + } + + private final String userHomeDir; + private final List hashOptionsPatterns; + + private ConfigTrackingValueTransformer(ConfigTrackingConfig config) { + userHomeDir = config.useUserHomeAliasInPaths() ? PropertyUtils.getUserHome() : null; + hashOptionsPatterns = config.getHashOptionsPatterns(); + } + + /** + * Returns a string value that can be persisted to file. + * + * @param name option name + * @param value configuration value + * @return string value that can be persisted to file + */ + public String transform(String name, ConfigValue value) { + return value == null ? NOT_CONFIGURED : transform(name, value.getValue()); + } + + /** + * Returns a string value that can be persisted to file. + * + * @param name option name + * @param original configuration value + * @return string value that can be persisted to file + */ + public String transform(String name, String original) { + if (original == null) { + return NOT_CONFIGURED; + } + + for (Pattern pattern : hashOptionsPatterns) { + if (pattern.matcher(name).matches()) { + return sha512(original); + } + } + + // replace user home path with an alias + if (userHomeDir != null && original.startsWith(userHomeDir)) { + var relativePath = original.substring(userHomeDir.length()); + if (relativePath.isEmpty()) { + return USER_HOME_DIR_ALIAS; + } + if (File.separator.equals(PATH_ELEMENT_SEPARATOR)) { + return USER_HOME_DIR_ALIAS + relativePath; + } + final StringJoiner joiner = new StringJoiner("/"); + joiner.add(USER_HOME_DIR_ALIAS); + var path = Path.of(relativePath); + for (int i = 0; i < path.getNameCount(); ++i) { + joiner.add(path.getName(i).toString()); + } + return joiner.toString(); + } + + return original; + } + + public static String sha512(String value) { + return sha512(value.getBytes(StandardCharsets.UTF_8)); + } + + public static String sha512(byte[] value) { + final byte[] digest = getSHA512().digest(value); + final StringBuilder sb = new StringBuilder(40); + for (int i = 0; i < digest.length; ++i) { + sb.append(Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1, 3)); + } + return sb.toString(); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingWriter.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingWriter.java new file mode 100644 index 0000000000000..20c51265c5032 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingWriter.java @@ -0,0 +1,110 @@ +package io.quarkus.deployment.configuration.tracker; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import io.quarkus.deployment.configuration.BuildTimeConfigurationReader; +import io.quarkus.runtime.LaunchMode; + +public class ConfigTrackingWriter { + + /** + * Checks whether a given configuration option matches at least one of the patterns. + * If the list of patterns is empty, the method will return false. + * + * @param name configuration option name + * @param patterns a list of name patterns + * @return true in case the option name matches at least one of the patterns, otherwise - false + */ + private static boolean matches(String name, List patterns) { + for (var pattern : patterns) { + if (pattern.matcher(name).matches()) { + return true; + } + } + return false; + } + + /** + * Configuration writer that will persist collected configuration options and their values + * to a file. + */ + public static void write(Map readOptions, ConfigTrackingConfig config, + BuildTimeConfigurationReader.ReadResult configReadResult, + LaunchMode launchMode, Path buildDirectory) { + if (!config.enabled()) { + return; + } + + Path file = config.file().orElse(null); + if (file == null) { + final Path dir = config.directory().orElseGet(() -> (buildDirectory.getParent() == null + ? buildDirectory + : buildDirectory.getParent()).resolve(".quarkus")); + file = dir + .resolve(config.filePrefix() + "-" + launchMode.getDefaultProfile() + config.fileSuffix()); + } else if (!file.isAbsolute()) { + file = config.directory().orElse(buildDirectory).resolve(file); + } + + if (file.getParent() != null) { + try { + Files.createDirectories(file.getParent()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + final List excludePatterns = config.getExcludePatterns(); + final ConfigTrackingValueTransformer valueTransformer = ConfigTrackingValueTransformer.newInstance(config); + + final Map allBuildTimeValues = configReadResult.getAllBuildTimeValues(); + final Map buildTimeRuntimeValues = configReadResult.getBuildTimeRunTimeValues(); + try (BufferedWriter writer = Files.newBufferedWriter(file)) { + final List names = new ArrayList<>(readOptions.size()); + for (var name : readOptions.keySet()) { + if ((allBuildTimeValues.containsKey(name) || buildTimeRuntimeValues.containsKey(name)) + && !matches(name, excludePatterns)) { + names.add(name); + } + } + Collections.sort(names); + for (String name : names) { + var value = valueTransformer.transform(name, readOptions.get(name)); + write(writer, name, value); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Writes a config option with its value to the target writer, + * possibly applying some transformations, such as character escaping + * prior to writing. + * + * @param writer target writer + * @param name option name + * @param value option value + * @throws IOException in case of a failure + */ + public static void write(Writer writer, String name, String value) throws IOException { + if (value != null) { + // escape the backslash before persisting + value = value.replace("\\", "\\\\"); + writer.write(name); + writer.write("="); + writer.write(value); + writer.write(System.lineSeparator()); + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java index b5695a88b1fda..ba98422a3893a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java @@ -51,7 +51,6 @@ import org.jboss.jandex.Type; import org.wildfly.common.Assert; -import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.proxy.ProxyConfiguration; import io.quarkus.deployment.proxy.ProxyFactory; import io.quarkus.deployment.recording.AnnotationProxyProvider.AnnotationProxy; @@ -71,7 +70,6 @@ import io.quarkus.runtime.StartupContext; import io.quarkus.runtime.StartupTask; import io.quarkus.runtime.annotations.IgnoreProperty; -import io.quarkus.runtime.annotations.RecordableConstructor; import io.quarkus.runtime.annotations.RelaxedValidation; /** @@ -114,7 +112,6 @@ public class BytecodeRecorderImpl implements RecorderContext { private final boolean staticInit; private final ClassLoader classLoader; - private static final Map, ProxyFactory> recordingProxyFactories = new ConcurrentHashMap<>(); private final Map, ProxyFactory> returnValueProxy = new ConcurrentHashMap<>(); private final Map, Object> existingProxyClasses = new ConcurrentHashMap<>(); @@ -364,9 +361,12 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl }; try { - if (recordingProxyFactories.containsKey(theClass)) { - return (T) recordingProxyFactories.get(theClass).newInstance(invocationHandler); + ProxyFactory factory = RecordingProxyFactories.get(theClass); + + if (factory != null) { + return factory.newInstance(invocationHandler); } + String proxyNameSuffix = "$$RecordingProxyProxy" + COUNT.incrementAndGet(); ProxyConfiguration proxyConfiguration = new ProxyConfiguration() @@ -374,19 +374,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl .setClassLoader(classLoader) .setAnchorClass(getClass()) .setProxyNameSuffix(proxyNameSuffix); - ProxyFactory factory = new ProxyFactory(proxyConfiguration); + factory = new ProxyFactory(proxyConfiguration); T recordingProxy = factory.newInstance(invocationHandler); existingProxyClasses.put(theClass, recordingProxy); - recordingProxyFactories.put(theClass, factory); - - if (theClass.getClassLoader() instanceof QuarkusClassLoader) { - ((QuarkusClassLoader) theClass.getClassLoader()).addCloseTask(new Runnable() { - @Override - public void run() { - recordingProxyFactories.remove(theClass); - } - }); - } + RecordingProxyFactories.put(theClass, factory); return recordingProxy; } catch (IllegalAccessException | InstantiationException e) { throw new RuntimeException(e); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingProxyFactories.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingProxyFactories.java new file mode 100644 index 0000000000000..29f39a82f0c86 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingProxyFactories.java @@ -0,0 +1,30 @@ +package io.quarkus.deployment.recording; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.deployment.proxy.ProxyFactory; + +class RecordingProxyFactories { + + private static final Map, ProxyFactory> RECORDING_PROXY_FACTORIES = new ConcurrentHashMap<>(); + + static void put(Class clazz, ProxyFactory proxyFactory) { + RECORDING_PROXY_FACTORIES.put(clazz, proxyFactory); + + if (clazz.getClassLoader() instanceof QuarkusClassLoader) { + ((QuarkusClassLoader) clazz.getClassLoader()).addCloseTask(new Runnable() { + @Override + public void run() { + RecordingProxyFactories.RECORDING_PROXY_FACTORIES.remove(clazz); + } + }); + } + } + + @SuppressWarnings("unchecked") + static ProxyFactory get(Class clazz) { + return (ProxyFactory) RECORDING_PROXY_FACTORIES.get(clazz); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 5403b4d14442e..743582f056a7d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -10,6 +10,7 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; +import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Modifier; import java.net.URI; @@ -48,6 +49,7 @@ import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; +import io.quarkus.deployment.builditem.QuarkusBuildCloseablesBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem; @@ -55,6 +57,10 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.configuration.BuildTimeConfigurationReader; import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator; +import io.quarkus.deployment.configuration.tracker.ConfigTrackingConfig; +import io.quarkus.deployment.configuration.tracker.ConfigTrackingWriter; +import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; +import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.gizmo.ClassCreator; @@ -442,6 +448,29 @@ void warnDifferentProfileUsedBetweenBuildAndRunTime(ConfigRecorder configRecorde configRecorder.handleNativeProfileChange(config.getProfiles()); } + @BuildStep(onlyIf = IsNormal.class) + void persistReadConfigOptions(BuildProducer dummy, + QuarkusBuildCloseablesBuildItem closeables, + LaunchModeBuildItem launchModeBuildItem, + BuildSystemTargetBuildItem buildSystemTargetBuildItem, + ConfigurationBuildItem configBuildItem, + ConfigTrackingConfig configTrackingConfig) { + var readOptionsProvider = configBuildItem.getReadResult().getReadOptionsProvider(); + if (readOptionsProvider != null) { + closeables.add(new Closeable() { + @Override + public void close() throws IOException { + ConfigTrackingWriter.write( + readOptionsProvider.getReadOptions(), + configTrackingConfig, + configBuildItem.getReadResult(), + launchModeBuildItem.getLaunchMode(), + buildSystemTargetBuildItem.getOutputDirectory()); + } + }); + } + } + private String appendProfileToFilename(Path path, String activeProfile) { String pathWithoutExtension = getPathWithoutExtension(path); return String.format("%s-%s.%s", pathWithoutExtension, activeProfile, getFileExtension(path)); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/HashUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/HashUtil.java index 4b42a4780ef1a..e70a7c6d65ddc 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/HashUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/HashUtil.java @@ -6,6 +6,20 @@ public final class HashUtil { + private static MessageDigest getMessageDigest(String alg) { + try { + return MessageDigest.getInstance(alg); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + private static void toHex(byte[] digest, StringBuilder sb) { + for (int i = 0; i < digest.length; ++i) { + sb.append(Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1, 3)); + } + } + private HashUtil() { } @@ -14,17 +28,10 @@ public static String sha1(String value) { } public static String sha1(byte[] value) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - byte[] digest = md.digest(value); - StringBuilder sb = new StringBuilder(40); - for (int i = 0; i < digest.length; ++i) { - sb.append(Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1, 3)); - } - return sb.toString(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } + final byte[] digest = getMessageDigest("SHA-1").digest(value); + var sb = new StringBuilder(40); + toHex(digest, sb); + return sb.toString(); } public static String sha256(String value) { @@ -32,16 +39,20 @@ public static String sha256(String value) { } public static String sha256(byte[] value) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] digest = md.digest(value); - StringBuilder sb = new StringBuilder(40); - for (int i = 0; i < digest.length; ++i) { - sb.append(Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1, 3)); - } - return sb.toString(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } + final byte[] digest = getMessageDigest("SHA-256").digest(value); + var sb = new StringBuilder(40); + toHex(digest, sb); + return sb.toString(); + } + + public static String sha512(String value) { + return sha512(value.getBytes(StandardCharsets.UTF_8)); + } + + public static String sha512(byte[] value) { + final byte[] digest = getMessageDigest("SHA-512").digest(value); + var sb = new StringBuilder(128); + toHex(digest, sb); + return sb.toString(); } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java index bbe090b33678c..0c7146ddb1678 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java @@ -9,9 +9,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; @@ -21,7 +19,6 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.eclipse.aether.repository.RemoteRepository; @@ -38,10 +35,6 @@ @Mojo(name = "build", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, threadSafe = true) public class BuildMojo extends QuarkusBootstrapMojo { - static final String PACKAGE_TYPE_PROP = "quarkus.package.type"; - static final String NATIVE_PROFILE_NAME = "native"; - static final String NATIVE_PACKAGE_TYPE = "native"; - @Component MavenProjectHelper projectHelper; @@ -125,14 +118,7 @@ protected void doExecute() throws MojoExecutionException { // Essentially what this does is to enable the native package type even if a different package type is set // in application properties. This is done to preserve what users expect to happen when // they execute "mvn package -Dnative" even if quarkus.package.type has been set in application.properties - if (!System.getProperties().containsKey(PACKAGE_TYPE_PROP) - && isNativeProfileEnabled(mavenProject())) { - Object packageTypeProp = mavenProject().getProperties().get(PACKAGE_TYPE_PROP); - String packageType = NATIVE_PACKAGE_TYPE; - if (packageTypeProp != null) { - packageType = packageTypeProp.toString(); - } - System.setProperty(PACKAGE_TYPE_PROP, packageType); + if (!setPackageTypeSystemPropertyIfNativeProfileEnabled()) { propertiesToClear.add(PACKAGE_TYPE_PROP); } @@ -193,17 +179,6 @@ && isNativeProfileEnabled(mavenProject())) { } } - boolean isNativeProfileEnabled(MavenProject mavenProject) { - // gotcha: mavenProject.getActiveProfiles() does not always contain all active profiles (sic!), - // but getInjectedProfileIds() does (which has to be "flattened" first) - Stream activeProfileIds = mavenProject.getInjectedProfileIds().values().stream().flatMap(List::stream); - if (activeProfileIds.anyMatch(NATIVE_PROFILE_NAME::equalsIgnoreCase)) { - return true; - } - // recurse into parent (if available) - return Optional.ofNullable(mavenProject.getParent()).map(this::isNativeProfileEnabled).orElse(false); - } - @Override public void setLog(Log log) { super.setLog(log); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java index a990e5d8ccdef..22891e8f44de4 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.function.Consumer; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -37,6 +38,9 @@ public class DependencyTreeMojo extends AbstractMojo { @Parameter(defaultValue = "${project}", readonly = true, required = true) protected MavenProject project; + @Parameter(defaultValue = "${session}", readonly = true) + protected MavenSession session; + @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true) private List repos; @@ -130,6 +134,7 @@ private void logTree(final Consumer log) throws MojoExecutionException { protected MavenArtifactResolver resolver() { return resolver == null ? resolver = workspaceProvider.createArtifactResolver(BootstrapMavenContext.config() + .setUserSettings(session.getRequest().getUserSettingsFile()) // The system needs to be initialized with the bootstrap model builder to properly interpolate system properties set on the command line // e.g. -Dquarkus.platform.version=xxx //.setRepositorySystem(repoSystem) diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index b06e1b16173dc..34d04f14e98d9 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -1137,6 +1137,7 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk, String bootstrap bootstrapProvider.close(); } else { final BootstrapMavenContextConfig mvnConfig = BootstrapMavenContext.config() + .setUserSettings(session.getRequest().getUserSettingsFile()) .setRemoteRepositories(repos) .setWorkspaceDiscovery(true) .setPreferPomsFromWorkspace(true) diff --git a/devtools/maven/src/main/java/io/quarkus/maven/GoOfflineMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/GoOfflineMojo.java index 4502351c87d9b..d76dee2638e73 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/GoOfflineMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/GoOfflineMojo.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Set; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -43,6 +44,9 @@ public class GoOfflineMojo extends AbstractMojo { @Parameter(defaultValue = "${project}", readonly = true, required = true) MavenProject project; + @Parameter(defaultValue = "${session}", readonly = true) + MavenSession session; + @Component RepositorySystem repoSystem; @@ -118,6 +122,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { private MavenArtifactResolver getResolver() throws MojoExecutionException { return workspaceProvider.createArtifactResolver(BootstrapMavenContext.config() + .setUserSettings(session.getRequest().getUserSettingsFile()) .setCurrentProject(project.getBasedir().toString()) .setRemoteRepositoryManager(remoteRepositoryManager) .setRemoteRepositories(repos) diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapMojo.java index f303f390b8ec4..4bbf1b902d4b4 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapMojo.java @@ -6,7 +6,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; +import java.util.stream.Stream; import org.apache.maven.AbstractMavenLifecycleParticipant; import org.apache.maven.execution.MavenSession; @@ -34,6 +36,10 @@ public abstract class QuarkusBootstrapMojo extends AbstractMojo { static final String CLOSE_BOOTSTRAPPED_APP = "closeBootstrappedApp"; + static final String NATIVE_PACKAGE_TYPE = "native"; + static final String NATIVE_PROFILE_NAME = "native"; + static final String PACKAGE_TYPE_PROP = "quarkus.package.type"; + @Component protected QuarkusBootstrapProvider bootstrapProvider; @@ -293,4 +299,36 @@ protected CuratedApplication bootstrapApplication(LaunchMode mode) throws MojoEx protected Properties getBuildSystemProperties(boolean quarkusOnly) throws MojoExecutionException { return bootstrapProvider.bootstrapper(this).getBuildSystemProperties(this, quarkusOnly); } + + /** + * Essentially what this does is to enable the native package type even if a different package type is set + * in application properties. This is done to preserve what users expect to happen when + * they execute "mvn package -Dnative" even if quarkus.package.type has been set in application.properties + * + * @return true if the package type system property was set, otherwise - false + */ + protected boolean setPackageTypeSystemPropertyIfNativeProfileEnabled() { + if (!System.getProperties().containsKey(PACKAGE_TYPE_PROP) + && isNativeProfileEnabled(mavenProject())) { + Object packageTypeProp = mavenProject().getProperties().get(PACKAGE_TYPE_PROP); + String packageType = NATIVE_PACKAGE_TYPE; + if (packageTypeProp != null) { + packageType = packageTypeProp.toString(); + } + System.setProperty(PACKAGE_TYPE_PROP, packageType); + return true; + } + return false; + } + + private boolean isNativeProfileEnabled(MavenProject mavenProject) { + // gotcha: mavenProject.getActiveProfiles() does not always contain all active profiles (sic!), + // but getInjectedProfileIds() does (which has to be "flattened" first) + Stream activeProfileIds = mavenProject.getInjectedProfileIds().values().stream().flatMap(List::stream); + if (activeProfileIds.anyMatch(NATIVE_PROFILE_NAME::equalsIgnoreCase)) { + return true; + } + // recurse into parent (if available) + return Optional.ofNullable(mavenProject.getParent()).map(this::isNativeProfileEnabled).orElse(false); + } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java index 36bcdf8908ca3..2ae61b88271c5 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java @@ -180,6 +180,9 @@ private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, Launch if (mode == LaunchMode.DEVELOPMENT || mode == LaunchMode.TEST || isWorkspaceDiscovery(mojo)) { return workspaceProvider.createArtifactResolver( BootstrapMavenContext.config() + // it's important to pass user settings in case the process was not launched using the original mvn script + // for example using org.codehaus.plexus.classworlds.launcher.Launcher + .setUserSettings(mojo.mavenSession().getRequest().getUserSettingsFile()) .setCurrentProject(mojo.mavenProject().getFile().toString()) .setPreferPomsFromWorkspace(true) .setProjectModelProvider(getProjectMap(mojo.mavenSession())::get)); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java index 16043a4d5aa76..62a7e1437acf6 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.List; +import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; @@ -52,6 +53,9 @@ public abstract class QuarkusProjectMojoBase extends AbstractMojo { @Parameter(defaultValue = "${project}") protected MavenProject project; + @Parameter(defaultValue = "${session}", readonly = true) + MavenSession session; + @Component protected RepositorySystem repoSystem; diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectStateMojoBase.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectStateMojoBase.java index 9007e7315bb61..62a346128a09c 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectStateMojoBase.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectStateMojoBase.java @@ -140,6 +140,7 @@ protected MavenArtifactResolver catalogArtifactResolver() throws MojoExecutionEx @Override protected MavenArtifactResolver initArtifactResolver() throws MojoExecutionException { return workspaceProvider.createArtifactResolver(BootstrapMavenContext.config() + .setUserSettings(session.getRequest().getUserSettingsFile()) .setRemoteRepositoryManager(remoteRepositoryManager) // The system needs to be initialized with the bootstrap model builder to properly interpolate system properties set on the command line // e.g. -Dquarkus.platform.version=xxx diff --git a/devtools/maven/src/main/java/io/quarkus/maven/TrackConfigChangesMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/TrackConfigChangesMojo.java new file mode 100644 index 0000000000000..e721cf89e80c7 --- /dev/null +++ b/devtools/maven/src/main/java/io/quarkus/maven/TrackConfigChangesMojo.java @@ -0,0 +1,161 @@ +package io.quarkus.maven; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.deployment.configuration.tracker.ConfigTrackingWriter; +import io.quarkus.runtime.LaunchMode; + +/** + * Maven goal that is executed before the {@link BuildMojo}. + * The goal looks for a file that contains build time configuration options read during the previous build. + * If that file exists, the goal will check whether the configuration options used during the previous build + * have changed in the current configuration and will persist their current values to another file, so that + * both configuration files could be compared by tools caching build goal outcomes to check whether the previous + * outcome of the {@link BuildMojo} needs to be rebuilt. + */ +@Mojo(name = "track-config-changes", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, threadSafe = true) +public class TrackConfigChangesMojo extends QuarkusBootstrapMojo { + + /** + * Skip the execution of this mojo + */ + @Parameter(defaultValue = "false", property = "quarkus.track-config-changes.skip") + boolean skip = false; + + @Parameter(property = "launchMode") + String mode; + + @Parameter(property = "quarkus.track-config-changes.outputDirectory", defaultValue = "${project.build.directory}") + File outputDirectory; + + @Parameter(property = "quarkus.track-config-changes.outputFile", required = false) + File outputFile; + + @Parameter(property = "quarkus.recorded-build-config.directory", defaultValue = "${basedir}/.quarkus") + File recordedBuildConfigDirectory; + + @Parameter(property = "quarkus.recorded-build-config.file", required = false) + File recordedBuildConfigFile; + + @Override + protected boolean beforeExecute() throws MojoExecutionException, MojoFailureException { + if (skip) { + getLog().info("Skipping config dump"); + return false; + } + return true; + } + + @Override + protected void doExecute() throws MojoExecutionException, MojoFailureException { + final String lifecyclePhase = mojoExecution.getLifecyclePhase(); + if (mode == null) { + if (lifecyclePhase == null) { + mode = "NORMAL"; + } else { + mode = lifecyclePhase.contains("test") ? "TEST" : "NORMAL"; + } + } + final LaunchMode launchMode = LaunchMode.valueOf(mode); + if (getLog().isDebugEnabled()) { + getLog().debug("Bootstrapping Quarkus application in mode " + launchMode); + } + + Path targetFile; + if (outputFile == null) { + targetFile = outputDirectory.toPath() + .resolve("quarkus-" + launchMode.getDefaultProfile() + "-config-check"); + } else if (outputFile.isAbsolute()) { + targetFile = outputFile.toPath(); + } else { + targetFile = outputDirectory.toPath().resolve(outputFile.toPath()); + } + + Path compareFile; + if (this.recordedBuildConfigFile == null) { + compareFile = recordedBuildConfigDirectory.toPath() + .resolve("quarkus-" + launchMode.getDefaultProfile() + "-config-dump"); + } else if (this.recordedBuildConfigFile.isAbsolute()) { + compareFile = this.recordedBuildConfigFile.toPath(); + } else { + compareFile = recordedBuildConfigDirectory.toPath().resolve(this.recordedBuildConfigFile.toPath()); + } + + if (!Files.exists(compareFile)) { + getLog().info(compareFile + " not found"); + return; + } + final Properties compareProps = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(compareFile)) { + compareProps.load(reader); + } catch (IOException e) { + throw new RuntimeException("Failed to read " + compareFile, e); + } + + CuratedApplication curatedApplication = null; + QuarkusClassLoader deploymentClassLoader = null; + final ClassLoader originalCl = Thread.currentThread().getContextClassLoader(); + Properties actualProps; + final boolean clearPackageTypeSystemProperty = setPackageTypeSystemPropertyIfNativeProfileEnabled(); + try { + curatedApplication = bootstrapApplication(launchMode); + deploymentClassLoader = curatedApplication.createDeploymentClassLoader(); + Thread.currentThread().setContextClassLoader(deploymentClassLoader); + + final Class codeGenerator = deploymentClassLoader.loadClass("io.quarkus.deployment.CodeGenerator"); + final Method dumpConfig = codeGenerator.getMethod("readCurrentConfigValues", ApplicationModel.class, String.class, + Properties.class, QuarkusClassLoader.class, Properties.class); + actualProps = (Properties) dumpConfig.invoke(null, curatedApplication.getApplicationModel(), + launchMode.name(), getBuildSystemProperties(true), + deploymentClassLoader, compareProps); + } catch (Exception any) { + throw new MojoExecutionException("Failed to bootstrap Quarkus application", any); + } finally { + System.clearProperty(PACKAGE_TYPE_PROP); + Thread.currentThread().setContextClassLoader(originalCl); + if (deploymentClassLoader != null) { + deploymentClassLoader.close(); + } + } + + final List names = new ArrayList<>(actualProps.stringPropertyNames()); + Collections.sort(names); + + final Path outputDir = targetFile.getParent(); + if (outputDir != null && !Files.exists(outputDir)) { + try { + Files.createDirectories(outputDir); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + try (BufferedWriter writer = Files.newBufferedWriter(targetFile)) { + for (var name : names) { + ConfigTrackingWriter.write(writer, name, actualProps.getProperty(name)); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/docs/downstreamdoc.java b/docs/downstreamdoc.java new file mode 100755 index 0000000000000..058fc840c4e19 --- /dev/null +++ b/docs/downstreamdoc.java @@ -0,0 +1,374 @@ +//usr/bin/env jbang "$0" "$@" ; exit $? + +//DEPS io.quarkus.platform:quarkus-bom:3.2.2.Final@pom +//DEPS io.quarkus:quarkus-picocli +//DEPS io.quarkus:quarkus-jackson +//DEPS com.fasterxml.jackson.dataformat:jackson-dataformat-yaml + +//JAVAC_OPTIONS -parameters +//JAVA_OPTIONS -Djava.util.logging.manager=org.jboss.logmanager.LogManager + +//Q:CONFIG quarkus.log.level=SEVERE +//Q:CONFIG quarkus.log.category."downstreamdoc".level=INFO +//Q:CONFIG quarkus.banner.enabled=false + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import picocli.CommandLine.Command; + +@Command(name = "downstreamdoc", mixinStandardHelpOptions = true) +public class downstreamdoc implements Runnable { + + private static final Logger LOG = Logger.getLogger(downstreamdoc.class); + + private static final Path SOURCE_DOC_PATH = Path.of("src", "main", "asciidoc"); + private static final Path DOC_PATH = Path.of("target", "asciidoc", "sources"); + private static final Path INCLUDES_PATH = DOC_PATH.resolve("_includes"); + private static final Path GENERATED_FILES_PATH = Path.of("..", "target", "asciidoc", "generated"); + private static final Path IMAGES_PATH = DOC_PATH.resolve("images"); + private static final Path TARGET_ROOT_DIRECTORY = Path.of("target", "downstream-tree"); + private static final Path TARGET_IMAGES_DIRECTORY = TARGET_ROOT_DIRECTORY.resolve("images"); + private static final Path TARGET_INCLUDES_DIRECTORY = TARGET_ROOT_DIRECTORY.resolve("_includes"); + private static final Path TARGET_GENERATED_DIRECTORY = TARGET_ROOT_DIRECTORY.resolve("_generated"); + private static final Path TARGET_LISTING = Path.of("target", "downstream-files.txt"); + private static final Set EXCLUDED_FILES = Set.of( + DOC_PATH.resolve("_attributes-local.adoc") + ); + + private static final String ADOC_SUFFIX = ".adoc"; + private static final Pattern XREF_PATTERN = Pattern.compile("xref:([^\\.#\\[ ]+)\\" + ADOC_SUFFIX); + private static final String SOURCE_BLOCK_PREFIX = "[source"; + private static final String SOURCE_BLOCK_DELIMITER = "--"; + + private static final String QUARKUS_IO_GUIDES_ATTRIBUTE = "{quarkusio-guides}"; + + private static final Map TABS_REPLACEMENTS = Map.of( + Pattern.compile( + "((\\*) [^\n]+\n\\+)?\n\\[source,\\s?xml,\\s?role=\"primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven\"\\]\n\\.pom.xml\n----\n((([^-]+\\-?)+\n)+?)----\n(\\+?)\n\\[source,\\s?gradle,\\s?role=\"secondary asciidoc-tabs-target-sync-gradle\"\\]\n\\.build.gradle\n----\n((([^-]+\\-?)+\n)+?)----", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE), + "$1\n$2* Using Maven:\n+\n--\n[source,xml]\n----\n$3----\n--\n+\n$2* Using Gradle:\n+\n--\n[source,gradle]\n----\n$7----\n--", + Pattern.compile( + "\\[source,\\s?bash,\\s?subs=attributes\\+,\\s?role=\"primary asciidoc-tabs-sync-cli\"\\]\n\\.CLI\n(----)\n((([^-]+\\-?\\-?)+\n)+?)(----)", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE), + "* Using the Quarkus CLI:\n+\n--\n[source, bash, subs=attributes+]\n----\n$2----\n--", + Pattern.compile( + "\\[source,\\s?bash,\\s?subs=attributes\\+,\\s?role=\"secondary asciidoc-tabs-sync-maven\"\\]\n\\.Maven\n(----)\n((([^-]+\\-?\\-?)+\n)+?)(----)", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE), + "* Using Maven:\n+\n--\n[source, bash, subs=attributes+]\n----\n$2----\n--", + Pattern.compile( + "\\[source,\\s?bash,\\s?subs=attributes\\+,\\s?role=\"secondary asciidoc-tabs-sync-gradle\"\\]\n\\.Gradle\n(----)\n((([^-]+\\-?\\-?)+\n)+?)(----)", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE), + "* Using Gradle:\n+\n--\n[source, bash, subs=attributes+]\n----\n$2----\n--", + Pattern.compile( + "\\[role=\"primary\\s?asciidoc-tabs-sync-cli\"\\]\n\\.CLI\n\\*\\*\\*\\*\n\\[source,\\s?bash,\\s?subs=attributes\\+\\]\n----\n((([^-]+\\-?\\-?)+\n)+?)----\n((([^*]+\\*?\\*?)+\n)+?)\\*\\*\\*\\*", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE), + "* Using the Quarkus CLI:\n+\n--\n[source, bash, subs=attributes+]\n----\n$1----\n$4--", + Pattern.compile( + "\\[role=\"secondary\\s?asciidoc-tabs-sync-maven\"\\]\n\\.Maven\n\\*\\*\\*\\*\n\\[source,\\s?bash,\\s?subs=attributes\\+\\]\n----\n((([^-]+\\-?\\-?)+\n)+?)----\n((([^*]+\\*?\\*?)+\n)+?)\\*\\*\\*\\*", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE), + "* Using Maven:\n+\n--\n[source, bash, subs=attributes+]\n----\n$1----\n$4--"); + + @Override + public void run() { + if (!Files.isDirectory(DOC_PATH)) { + LOG.error("Transformed AsciiDoc sources directory does not exist. Have you built the documentation?"); + } + if (!Files.isDirectory(GENERATED_FILES_PATH)) { + LOG.error("Generated files directory does not exist. Have you built the documentation?"); + } + + try { + deleteDirectory(TARGET_ROOT_DIRECTORY); + Files.deleteIfExists(TARGET_LISTING); + + ObjectMapper yamlObjectMapper = new ObjectMapper(new YAMLFactory()); + yamlObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + ConfigFile configFile = yamlObjectMapper.readValue(new File("downstreamdoc.yaml"), ConfigFile.class); + + Set guides = new TreeSet<>(); + Set simpleIncludes = new TreeSet<>(); + Set includes = new TreeSet<>(); + Set generatedFiles = new TreeSet<>(); + Set images = new TreeSet<>(); + + Set allResolvedPaths = new TreeSet<>(); + + Set downstreamGuides = new TreeSet<>(); + + for (String guide : new TreeSet<>(configFile.guides)) { + Path guidePath = DOC_PATH.resolve(SOURCE_DOC_PATH.relativize(Path.of(guide))); + + if (!Files.isReadable(guidePath)) { + LOG.error("Unable to read file " + guidePath); + continue; + } + + downstreamGuides.add(guidePath.getFileName().toString()); + allResolvedPaths.add(guidePath); + + GuideContent guideContent = new GuideContent(guidePath); + getFiles(guideContent, guidePath); + + guides.add(guidePath); + simpleIncludes.addAll(guideContent.simpleIncludes); + includes.addAll(guideContent.includes); + generatedFiles.addAll(guideContent.generatedFiles); + images.addAll(guideContent.images); + } + + Files.createDirectories(TARGET_ROOT_DIRECTORY); + + for (Path guide : guides) { + copyAsciidoc(guide, TARGET_ROOT_DIRECTORY.resolve(guide.getFileName()), downstreamGuides); + } + for (Path simpleInclude : simpleIncludes) { + Path sourceFile = DOC_PATH.resolve(simpleInclude); + + if (EXCLUDED_FILES.contains(sourceFile)) { + continue; + } + if (!Files.isReadable(sourceFile)) { + LOG.error("Unable to read include " + sourceFile); + } + allResolvedPaths.add(sourceFile); + Path targetFile = TARGET_ROOT_DIRECTORY.resolve(simpleInclude); + Files.createDirectories(targetFile.getParent()); + copyAsciidoc(sourceFile, targetFile, downstreamGuides); + } + for (Path include : includes) { + Path sourceFile = INCLUDES_PATH.resolve(include); + if (EXCLUDED_FILES.contains(sourceFile)) { + continue; + } + if (!Files.isReadable(sourceFile)) { + LOG.error("Unable to read include " + sourceFile); + } + allResolvedPaths.add(sourceFile); + Path targetFile = TARGET_INCLUDES_DIRECTORY.resolve(include); + Files.createDirectories(targetFile.getParent()); + copyAsciidoc(sourceFile, targetFile, downstreamGuides); + } + for (Path generatedFile : generatedFiles) { + Path sourceFile = GENERATED_FILES_PATH.resolve(generatedFile); + if (EXCLUDED_FILES.contains(sourceFile)) { + continue; + } + if (!Files.isReadable(sourceFile)) { + LOG.error("Unable to read generated file " + sourceFile); + } + allResolvedPaths.add(sourceFile); + Path targetFile = TARGET_GENERATED_DIRECTORY.resolve(generatedFile); + Files.createDirectories(targetFile.getParent()); + copyAsciidoc(sourceFile, targetFile, downstreamGuides); + } + for (Path image : images) { + Path sourceFile = IMAGES_PATH.resolve(image); + if (EXCLUDED_FILES.contains(sourceFile)) { + continue; + } + if (!Files.isReadable(sourceFile)) { + LOG.error("Unable to read image " + sourceFile); + } + allResolvedPaths.add(sourceFile); + Path targetFile = TARGET_IMAGES_DIRECTORY.resolve(image); + Files.createDirectories(targetFile.getParent()); + Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + + Files.writeString(TARGET_LISTING, allResolvedPaths.stream().map(p -> p.toString()).collect(Collectors.joining("\n"))); + + LOG.info("Downstream documentation tree is available in: " + TARGET_ROOT_DIRECTORY); + LOG.info("Downstream documentation listing is available in: " + TARGET_LISTING); + } catch (IOException e) { + LOG.error("An error occurred while generating the downstream tree", e); + System.exit(1); + } + } + + private static void getFiles(GuideContent guideContent, Path currentFile) throws IOException { + List lines = Files.readAllLines(currentFile); + + for (String line : lines) { + Optional possibleInclude = extractPath(line, "include::{includes}"); + if (possibleInclude.isPresent()) { + guideContent.includes.add(possibleInclude.get()); + getFurtherIncludes(guideContent, INCLUDES_PATH.resolve(possibleInclude.get())); + continue; + } + Optional possibleGeneratedFile = extractPath(line, "include::{generated-dir}"); + if (possibleGeneratedFile.isPresent()) { + guideContent.generatedFiles.add(possibleGeneratedFile.get()); + continue; + } + Optional possibleSimpleInclude = extractPath(line, "include::"); + if (possibleSimpleInclude.isPresent()) { + guideContent.simpleIncludes.add(possibleSimpleInclude.get()); + getFiles(guideContent, currentFile.getParent().resolve(possibleSimpleInclude.get())); + continue; + } + Optional possibleImage = extractPath(line, "image::"); + if (possibleImage.isPresent()) { + guideContent.images.add(possibleImage.get()); + continue; + } + } + } + + private static void getFurtherIncludes(GuideContent guideContent, Path currentFile) throws IOException { + List lines = Files.readAllLines(currentFile); + + for (String line : lines) { + Optional possibleInclude = extractPath(line, "include::"); + if (possibleInclude.isPresent()) { + guideContent.includes.add(possibleInclude.get()); + getFurtherIncludes(guideContent, currentFile.getParent().resolve(possibleInclude.get())); + continue; + } + Optional possibleImage = extractPath(line, "image::"); + if (possibleImage.isPresent()) { + guideContent.images.add(possibleImage.get()); + continue; + } + } + } + + private static Optional extractPath(String asciidoc, String prefix) { + if (!asciidoc.startsWith(prefix)) { + return Optional.empty(); + } + + String path = asciidoc.substring(prefix.length(), asciidoc.indexOf('[')); + + if (path.startsWith("/")) { + path = path.substring(1); + } + + return Optional.of(Path.of(path)); + } + + private static void deleteDirectory(Path directory) throws IOException { + if (!Files.isDirectory(directory)) { + return; + } + + Files.walk(directory) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + + private void copyAsciidoc(Path sourceFile, Path targetFile, Set downstreamGuides) throws IOException { + List guideLines = Files.readAllLines(sourceFile); + + StringBuilder rewrittenGuide = new StringBuilder(); + StringBuilder currentBuffer = new StringBuilder(); + boolean inSourceBlock = false; + boolean findDelimiter = false; + String currentSourceBlockDelimiter = "----"; + int lineNumber = 0; + + for (String line : guideLines) { + lineNumber++; + + if (inSourceBlock) { + if (findDelimiter) { + rewrittenGuide.append(line + "\n"); + if (line.isBlank() || line.startsWith(".")) { + continue; + } + if (!line.startsWith(SOURCE_BLOCK_DELIMITER)) { + throw new IllegalStateException("Unable to find source block delimiter in file " + + sourceFile + " at line " + lineNumber); + } + currentSourceBlockDelimiter = line.stripTrailing(); + findDelimiter = false; + continue; + } + + if (line.stripTrailing().equals(currentSourceBlockDelimiter)) { + inSourceBlock = false; + } + rewrittenGuide.append(line + "\n"); + continue; + } + if (line.startsWith(SOURCE_BLOCK_PREFIX)) { + inSourceBlock = true; + findDelimiter = true; + + if (currentBuffer.length() > 0) { + rewrittenGuide.append( + rewriteLinks(currentBuffer.toString(), downstreamGuides)); + currentBuffer.setLength(0); + } + rewrittenGuide.append(line + "\n"); + continue; + } + + currentBuffer.append(line + "\n"); + } + + if (currentBuffer.length() > 0) { + rewrittenGuide + .append(rewriteLinks(currentBuffer.toString(), downstreamGuides)); + } + + String rewrittenGuideWithoutTabs = rewrittenGuide.toString().trim(); + + for (Entry tabReplacement : TABS_REPLACEMENTS.entrySet()) { + rewrittenGuideWithoutTabs = tabReplacement.getKey().matcher(rewrittenGuideWithoutTabs).replaceAll(tabReplacement.getValue()); + } + + Files.writeString(targetFile, rewrittenGuideWithoutTabs.trim()); + } + + private String rewriteLinks(String content, Set downstreamGuides) { + content = XREF_PATTERN.matcher(content).replaceAll(mr -> { + if (downstreamGuides.contains(mr.group(1) + ADOC_SUFFIX)) { + return mr.group(0); + } + + return "link:" + QUARKUS_IO_GUIDES_ATTRIBUTE + "/" + mr.group(1); + }); + + return content; + } + + public static class GuideContent { + + public Path guide; + public Set simpleIncludes = new TreeSet<>(); + public Set includes = new TreeSet<>(); + public Set images = new TreeSet<>(); + public Set generatedFiles = new TreeSet<>(); + + public GuideContent(Path guide) { + this.guide = guide; + } + } + + public static class ConfigFile { + + public List guides; + } +} diff --git a/docs/downstreamdoc.yaml b/docs/downstreamdoc.yaml new file mode 100644 index 0000000000000..41c96ef42024d --- /dev/null +++ b/docs/downstreamdoc.yaml @@ -0,0 +1,22 @@ +guides: + - src/main/asciidoc/datasource.adoc + - src/main/asciidoc/deploying-to-openshift.adoc + - src/main/asciidoc/logging.adoc + - src/main/asciidoc/security-architecture.adoc + - src/main/asciidoc/security-authentication-mechanisms.adoc + - src/main/asciidoc/security-authorize-web-endpoints-reference.adoc + - src/main/asciidoc/security-basic-authentication.adoc + - src/main/asciidoc/security-basic-authentication-howto.adoc + - src/main/asciidoc/security-basic-authentication-tutorial.adoc + - src/main/asciidoc/security-identity-providers.adoc + - src/main/asciidoc/security-jpa.adoc + - src/main/asciidoc/security-oidc-bearer-token-authentication.adoc + - src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc + - src/main/asciidoc/security-oidc-code-flow-authentication.adoc + - src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc + - src/main/asciidoc/security-oidc-configuration-properties-reference.adoc + - src/main/asciidoc/security-openid-connect-client-reference.adoc + - src/main/asciidoc/security-overview.adoc + - src/main/asciidoc/security-proactive-authentication.adoc + - src/main/asciidoc/security-vulnerability-detection.adoc + - src/main/asciidoc/update-quarkus.adoc diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc index 610ab5295bd07..53dc2eed166af 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/src/main/asciidoc/_attributes.adoc @@ -19,7 +19,7 @@ :kibana-image: ${kibana.image} :keycloak-docker-image: ${keycloak.docker.image} :jandex-version: ${jandex.version} -:jandex-gradle-plugin.version: ${jandex-gradle-plugin.version} +:jandex-gradle-plugin-version: ${jandex-gradle-plugin.version} :kotlin-version: ${kotlin.version} :grpc-version: ${grpc.version} :protoc-version: ${protoc.version} diff --git a/docs/src/main/asciidoc/_includes/devtools/build-native-container-parameters.adoc b/docs/src/main/asciidoc/_includes/devtools/build-native-container-parameters.adoc index c02805cfa22cc..d30f1c353367f 100644 --- a/docs/src/main/asciidoc/_includes/devtools/build-native-container-parameters.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/build-native-container-parameters.adoc @@ -1,11 +1,11 @@ -[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- quarkus build --native -Dquarkus.native.container-build=true {build-additional-parameters} ---- ifndef::devtools-no-maven[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] .Maven ---- ./mvnw install -Dnative -Dquarkus.native.container-build=true {build-additional-parameters} @@ -13,7 +13,7 @@ ifdef::devtools-wrapped[+] endif::[] ifndef::devtools-no-gradle[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] .Gradle ---- ./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true {build-additional-parameters} diff --git a/docs/src/main/asciidoc/_includes/devtools/build-native-container.adoc b/docs/src/main/asciidoc/_includes/devtools/build-native-container.adoc index 8aa4202cc97fd..fa6ddc6b51e41 100644 --- a/docs/src/main/asciidoc/_includes/devtools/build-native-container.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/build-native-container.adoc @@ -1,4 +1,4 @@ -[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- ifdef::build-additional-parameters[] @@ -11,7 +11,7 @@ endif::[] ---- ifndef::devtools-no-maven[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] .Maven ---- ifdef::build-additional-parameters[] @@ -24,7 +24,7 @@ endif::[] endif::[] ifndef::devtools-no-gradle[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] .Gradle ---- ifdef::build-additional-parameters[] diff --git a/docs/src/main/asciidoc/_includes/devtools/build-native.adoc b/docs/src/main/asciidoc/_includes/devtools/build-native.adoc index 4531e92616050..4312338484d46 100644 --- a/docs/src/main/asciidoc/_includes/devtools/build-native.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/build-native.adoc @@ -1,4 +1,4 @@ -[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- ifdef::build-additional-parameters[] @@ -10,7 +10,7 @@ endif::[] ---- ifndef::devtools-no-maven[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] .Maven ---- ifdef::build-additional-parameters[] @@ -23,7 +23,7 @@ endif::[] endif::[] ifndef::devtools-no-gradle[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] .Gradle ---- ifdef::build-additional-parameters[] diff --git a/docs/src/main/asciidoc/_includes/devtools/build.adoc b/docs/src/main/asciidoc/_includes/devtools/build.adoc index 480621358d23e..57dc4b980a529 100644 --- a/docs/src/main/asciidoc/_includes/devtools/build.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/build.adoc @@ -1,4 +1,4 @@ -[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- ifdef::build-additional-parameters[] diff --git a/docs/src/main/asciidoc/_includes/devtools/create-app.adoc b/docs/src/main/asciidoc/_includes/devtools/create-app.adoc index 8edd0676f501a..b09873d6687ee 100644 --- a/docs/src/main/asciidoc/_includes/devtools/create-app.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/create-app.adoc @@ -52,7 +52,7 @@ ifndef::devtools-no-gradle[] To create a Gradle project, add the `--gradle` or `--gradle-kotlin-dsl` option. endif::[] -_For more information about how to install the Quarkus CLI and use it, please refer to xref:cli-tooling.adoc[the Quarkus CLI guide]._ +For more information about how to install and use the Quarkus CLI, see the xref:cli-tooling.adoc[Quarkus CLI] guide. **** [role="secondary asciidoc-tabs-sync-maven"] diff --git a/docs/src/main/asciidoc/_includes/devtools/create-cli.adoc b/docs/src/main/asciidoc/_includes/devtools/create-cli.adoc index 770cce8c4447d..972f5097c2c4f 100644 --- a/docs/src/main/asciidoc/_includes/devtools/create-cli.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/create-cli.adoc @@ -50,7 +50,7 @@ endif::[] To create a Gradle project, add the `--gradle` or `--gradle-kotlin-dsl` option. -_For more information about how to install the Quarkus CLI and use it, please refer to xref:cli-tooling.adoc[the Quarkus CLI guide]._ +For more information about how to install and use the Quarkus CLI, see the xref:cli-tooling.adoc[Quarkus CLI] guide. **** [role="secondary asciidoc-tabs-sync-maven"] diff --git a/docs/src/main/asciidoc/_includes/devtools/dev-parameters.adoc b/docs/src/main/asciidoc/_includes/devtools/dev-parameters.adoc index 4773ee969f552..8b6d33ab03501 100644 --- a/docs/src/main/asciidoc/_includes/devtools/dev-parameters.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/dev-parameters.adoc @@ -1,11 +1,11 @@ -[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- quarkus dev {dev-additional-parameters} ---- ifndef::devtools-no-maven[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] .Maven ---- ./mvnw quarkus:dev {dev-additional-parameters} @@ -13,7 +13,7 @@ ifdef::devtools-wrapped[+] endif::[] ifndef::devtools-no-gradle[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] .Gradle ---- ./gradlew --console=plain quarkusDev {dev-additional-parameters} diff --git a/docs/src/main/asciidoc/_includes/devtools/dev.adoc b/docs/src/main/asciidoc/_includes/devtools/dev.adoc index 86bbddb48949a..1cbf6b88cf992 100644 --- a/docs/src/main/asciidoc/_includes/devtools/dev.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/dev.adoc @@ -1,4 +1,4 @@ -[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- ifdef::dev-additional-parameters[] @@ -10,7 +10,7 @@ endif::[] ---- ifdef::devtools-wrapped[+] ifndef::devtools-no-maven[] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] .Maven ---- ifdef::dev-additional-parameters[] @@ -23,7 +23,7 @@ endif::[] endif::[] ifdef::devtools-wrapped[+] ifndef::devtools-no-gradle[] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] .Gradle ---- ifdef::dev-additional-parameters[] diff --git a/docs/src/main/asciidoc/_includes/devtools/extension-add.adoc b/docs/src/main/asciidoc/_includes/devtools/extension-add.adoc index e12efaf42181c..ff24545861b07 100644 --- a/docs/src/main/asciidoc/_includes/devtools/extension-add.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/extension-add.adoc @@ -1,4 +1,4 @@ -[source,bash,subs=attributes+,role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- quarkus extension add '{add-extension-extensions}' @@ -13,7 +13,7 @@ ifdef::devtools-wrapped[+] endif::[] ifndef::devtools-no-gradle[] ifdef::devtools-wrapped[+] -[source,bash,subs=attributes+,role="secondary asciidoc-tabs-sync-gradle"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] .Gradle ---- ./gradlew addExtension --extensions='{add-extension-extensions}' diff --git a/docs/src/main/asciidoc/_includes/devtools/extension-list.adoc b/docs/src/main/asciidoc/_includes/devtools/extension-list.adoc index 525271a8fc7d9..b4a26966bd58a 100644 --- a/docs/src/main/asciidoc/_includes/devtools/extension-list.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/extension-list.adoc @@ -1,4 +1,4 @@ -[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- quarkus extension @@ -13,7 +13,7 @@ ifdef::devtools-wrapped[+] endif::[] ifndef::devtools-no-gradle[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] .Gradle ---- ./gradlew listExtensions diff --git a/docs/src/main/asciidoc/_includes/devtools/maven-opts.adoc b/docs/src/main/asciidoc/_includes/devtools/maven-opts.adoc index 5df51026e1521..78348b1353733 100644 --- a/docs/src/main/asciidoc/_includes/devtools/maven-opts.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/maven-opts.adoc @@ -1,11 +1,11 @@ -[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-cli"] +[source,bash,subs=attributes+, role="primary asciidoc-tabs-sync-cli"] .CLI ---- MAVEN_OPTS='--enable-preview' quarkus build ---- ifndef::devtools-no-maven[] ifdef::devtools-wrapped[+] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] .Maven ---- MAVEN_OPTS='--enable-preview' ./mvnw install diff --git a/docs/src/main/asciidoc/_includes/devtools/test.adoc b/docs/src/main/asciidoc/_includes/devtools/test.adoc index c690c4a946e65..320187f70e9ca 100644 --- a/docs/src/main/asciidoc/_includes/devtools/test.adoc +++ b/docs/src/main/asciidoc/_includes/devtools/test.adoc @@ -1,5 +1,5 @@ ifndef::devtools-no-maven[] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-maven"] .Maven ---- ./mvnw test @@ -7,7 +7,7 @@ ifndef::devtools-no-maven[] endif::[] ifdef::devtools-wrapped[+] ifndef::devtools-no-gradle[] -[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +[source,bash,subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] .Gradle ---- ./gradlew test diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index f58090e488b6f..f91f1f401797c 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -82,7 +82,7 @@ plugins { id 'org.kordamp.gradle.jandex' version '{jandex-gradle-plugin-version}' } ---- - +You can find the latest plugin version in the https://plugins.gradle.org/plugin/org.kordamp.gradle.jandex[Gradle Plugin Portal] **** [role="secondary asciidoc-tabs-sync-gradle-kotlin"] @@ -95,6 +95,8 @@ plugins { id("org.kordamp.gradle.jandex") version '{jandex-gradle-plugin-version}' } ---- +You can find the latest plugin version in the https://plugins.gradle.org/plugin/org.kordamp.gradle.jandex[Gradle Plugin Portal] + **** diff --git a/docs/src/main/asciidoc/cli-tooling.adoc b/docs/src/main/asciidoc/cli-tooling.adoc index fa811bdf19c64..ee4e32c23b7f5 100644 --- a/docs/src/main/asciidoc/cli-tooling.adoc +++ b/docs/src/main/asciidoc/cli-tooling.adoc @@ -4,7 +4,6 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Building Quarkus apps with Quarkus Command Line Interface (CLI) -:extension-status: preview include::_attributes.adoc[] :categories: tooling :summary: Use the Quarkus CLI to create, build, run, and manage extensions for Quarkus projects. @@ -12,8 +11,6 @@ include::_attributes.adoc[] The `quarkus` command lets you create projects, manage extensions and do essential build and development tasks using the underlying project build tool. -include::{includes}/extension-status.adoc[] - == Installing the CLI The Quarkus CLI is available in several developer-oriented package managers such as: diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 5b78ff8f33194..3e7308792d2c8 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -621,7 +621,7 @@ extensions. Therefore, the `quarkus.` prefix should **never** be used for applic === Build Time configuration -Some Quarkus configurations only take effect during build time, meaning is not possible to change them at runtime. These +Some Quarkus configurations only take effect during build time, meaning it is not possible to change them at runtime. These configurations are still available at runtime but as read-only and have no effect in Quarkus behaviour. A change to any of these configurations requires a rebuild of the application itself to reflect changes of such properties. @@ -635,6 +635,63 @@ application behaviour at runtime. If you are in the rare situation that you need to change the build time configuration after your application is built, then check out how xref:reaugmentation.adoc[re-augmentation] can be used to rebuild the augmentation output for a different build time configuration. +== Tracking effective build time configuration used at build time + +Given that configuration sources usually provide more options than actually used during the build, it might be useful to know which configuration options have actually been used during a Quarkus build process. + +=== Dumping build time configuration options read during the build + +Setting `quarkus.config-tracking.enabled` to `true` will enable a configuration interceptor that will record every configuration option that was read during the build process along with their values. The resulting report will be stored in `${project.basedir}/.quarkus/quarkus-prod-config-dump` by default. The target file could be configured using the following options: + +* `quarkus.config-tracking.directory` - directory in which the configuration dump should be stored, the default is `${project.basedir}/.quarkus` +* `quarkus.config-tracking.file-prefix` - file name prefix, the default value is `quarkus` +* `quarkus.config-tracking.file-suffix` - file name suffix, the default value is `-config-dump` +* `quarkus.config-tracking.file` - path to a file in which the configuration dump should be stored. This option supersedes the `file-prefix` and `file-suffix` options. Also supersedes the value of `quarkus.config-tracking.directory`, unless the value is a relative path. + +The `prod` part of the `quarkus-prod-config-dump` file name refers to the Quarkus build mode, indicating that the dump was taken for the production build. + +The reason `${project.basedir}/.quarkus` directory was chosen as the default location was to make it easy to track build time configuration changes between builds and use that as an indicator to build output caching tools (such as https://maven.apache.org/extensions/maven-build-cache-extension/[Apache Maven Build Cache] and https://gradle.com/gradle-enterprise-solutions/build-cache/[Gradle Enterprise Build Cache]) whether the application binary has to be re-built. + +==== Filtering configuration options + +Configuration tracker could be instructed to exclude some of the options from the report by configuring `quarkus.config-tracking.exclude` with a comma-separated list of configuration option names that should be filtered out. + +==== Path values + +Configuration options with *absolute* path values that begin with a user home directory are, by default, transformed with Unix home directory alias '~' replacing the user home directory part and using `/` as a path element separator. + +This transformation can be disabled by setting `quarkus.config-tracking.use-user-home-alias-in-paths` to `false`. + +==== Hashing recorded configuration values + +Configuration values can be hashed using `SHA-512` algorithm before they are written to a file. Configuration option names whose values should be hashed can be configured in `quarkus.config-tracking.hash-options` as a comma separated list. + +=== Tracking build time configuration changes between builds + +While `quarkus.config-tracking.enabled` enables effective build time configuration report generation, there is also a way to check whether the values stored in that report have changed before the next build of the project is launched. + +Maven projects could add the following goal to their `quarkus-maven-plugin` configuration: +[source,xml] +---- + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + track-prod-config-changes + process-resources + + track-config-changes + + + +---- + +The `track-config-changes` goal looks for `${project.basedir}/.quarkus/quarkus-prod-config-dump` (file name and directory are configurable) and, if the file already exists, checks whether the values stored in the config dump have changed. +It will log the changed options and save the current values of each of the options present in `${project.basedir}/.quarkus/quarkus-prod-config-dump` in `${project.basedir}/target/quarkus-prod-config.check` (the target file name and location can be configured). If the build time configuration has not changed since the last build both `${project.basedir}/.quarkus/quarkus-prod-config-dump` and `${project.basedir}/.quarkus/quarkus-prod-config-dump` will be identical. + [[additional-information]] == Additional Information diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 0603af9ef0855..e1806fd43149c 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -144,7 +144,7 @@ don't want to use an annotation. Each endpoint method must be annotated with one of the following annotations, which defines which HTTP method will be mapped to the method: -.Table HTTP method annotations +.HTTP method annotations |=== |Annotation|Usage @@ -234,7 +234,7 @@ or `` or `` (https://maven.apache.org/plu The following HTTP request elements may be obtained by your endpoint method: -.Table HTTP request parameter annotations +.HTTP request parameter annotations |=== |HTTP element|Annotation|Usage @@ -365,26 +365,26 @@ public class Endpoint { public static class Parameters { @RestPath String type; - + @RestMatrix String variant; - + @RestQuery String age; - + @RestCookie String level; - + @RestHeader("X-Cheese-Secret-Handshake") String secretHandshake; - + @RestForm String smell; } @POST public String allParams(@BeanParam Parameters parameters) { <1> - return parameters.type + "/" + parameters.variant + "/" + parameters.age + return parameters.type + "/" + parameters.variant + "/" + parameters.age + "/" + parameters.level + "/" + parameters.secretHandshake + "/" + parameters.smell; } @@ -434,7 +434,7 @@ The following parameter types will be supported out of the box: [[resource-types]] -.Table Request body parameter type +.Request body parameter types |=== |Type|Usage @@ -484,7 +484,7 @@ link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/RestForm.html[`@ that allow you to access the parts as files or as entities. Let us look at an example of its use. -Assuming an HTTP request containing a file upload, a JSON entity and a form value containing a string description, we could write +Assuming an HTTP request containing a file upload, a JSON entity and a form value containing a string description, we could write the following endpoint: [source,java] @@ -503,7 +503,7 @@ public class MultipartResource { public String firstName; public String lastName; } - + @POST public void multipart(@RestForm String description, @RestForm("image") FileUpload file, @@ -670,7 +670,7 @@ public class Endpoint { } ---- -This last approach allows you adding extra headers to the output part. +This last approach allows you adding extra headers to the output part. WARNING: For the time being, returning Multipart data is limited to be blocking endpoints. @@ -685,7 +685,7 @@ and any other type will be mapped <>. In addition, the following return types are also supported: -.Table Additional response body parameter type +.Additional response body parameter types |=== |Type|Usage @@ -1075,7 +1075,7 @@ public class Endpoint { @Inject @Channel("book-out") Multi books; - + @Inject Sse sse; <1> @@ -1121,7 +1121,7 @@ NOTE: More information on the `Cache-Control` header and be found in link:https: There are a number of contextual objects that the framework will give you, if your endpoint method takes parameters of the following type: -.Table Context object +.Contextual objects |=== |Type|Usage @@ -1258,7 +1258,6 @@ public class Endpoint { Instead of importing `io.quarkus:quarkus-resteasy-reactive`, you can import either of the following modules to get support for JSON: -.Table Context object |=== |GAV|Usage @@ -1489,7 +1488,6 @@ public static class SupportUnquotedFields implements BiFunction>. -The JAXB Resteasy Reactive extension will automatically detect the classes that are used in the resources and require JAXB serialization. Then, it will register these classes into the default `JAXBContext` which is internally used by the JAXB message reader and writer. +The JAXB Resteasy Reactive extension will automatically detect the classes that are used in the resources and require JAXB serialization. Then, it will register these classes into the default `JAXBContext` which is internally used by the JAXB message reader and writer. -However, in some situations, these classes cause the `JAXBContext` to fail: for example, when you're using the same class name in different java packages. In these cases, the application will fail at build time and print the JAXB exception that caused the issue, so you can properly fix it. Alternatively, you can also exclude the classes that cause the issue by using the property `quarkus.jaxb.exclude-classes`. When excluding classes that are required by any resource, the JAXB resteasy reactive extension will create and cache a custom `JAXBContext` that will include the excluded class, causing a minimal performance degradance. +However, in some situations, these classes cause the `JAXBContext` to fail: for example, when you're using the same class name in different java packages. In these cases, the application will fail at build time and print the JAXB exception that caused the issue, so you can properly fix it. Alternatively, you can also exclude the classes that cause the issue by using the property `quarkus.jaxb.exclude-classes`. When excluding classes that are required by any resource, the JAXB resteasy reactive extension will create and cache a custom `JAXBContext` that will include the excluded class, causing a minimal performance degradance. [NOTE] ==== -The property `quarkus.jaxb.exclude-classes` accepts a comma separated list of fully qualified class names, for example: `quarkus.jaxb.exclude-classes=org.acme.one.Model,org.acme.two.Model`. +The property `quarkus.jaxb.exclude-classes` accepts a comma separated list of either fully qualified class names +or package names. Package names must be suffixed by `.*` and all classes in the specified package and its subpackages will be excluded. + +For instance, when setting `quarkus.jaxb.exclude-classes=org.acme.one.Model,org.acme.two.Model,org.acme.somemodel.*`, the following elements are excluded: + +- The class `org.acme.one.Model` +- The class `org.acme.two.Model` +- All the classes in the `org.acme.somemodel` package and its subpackages ==== ==== Advanced JAXB-specific features @@ -1604,7 +1609,6 @@ Note that if you provide your custom JAXB context instance, you will need to reg To enable Web Links support, add the `quarkus-resteasy-reactive-links` extension to your project. -.Table Context object |=== |GAV|Usage @@ -1720,7 +1724,6 @@ The https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] standard is a si To enable the HAL support, add the `quarkus-hal` extension to your project. Also, as HAL needs JSON support, you need to add either the `quarkus-resteasy-reactive-jsonb` or the `quarkus-resteasy-reactive-jackson` extension. -.Table Context object |=== |GAV|Usage @@ -2100,7 +2103,7 @@ You can also declare link:{jaxrsspec}#exceptionmapper[exception mappers in the J Your exception mapper may declare any of the following parameter types: -.Table Exception mapper parameters +.Exception mapper parameters |=== |Type|Usage @@ -2117,7 +2120,7 @@ Your exception mapper may declare any of the following parameter types: It may declare any of the following return types: -.Table Exception mapper return types +.Exception mapper return types |=== |Type|Usage @@ -2217,7 +2220,7 @@ class Filters { Your filters may declare any of the following parameter types: -.Table Filter parameters +.Filter parameters |=== |Type|Usage @@ -2237,7 +2240,7 @@ Your filters may declare any of the following parameter types: It may declare any of the following return types: -.Table Filter return types +.Filter return types |=== |Type|Usage diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java index d7966a647e460..f7a7040fe886d 100644 --- a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java +++ b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java @@ -239,7 +239,7 @@ void generateDataSourceSupportBean(AgroalRecorder recorder, .setDefaultScope(DotNames.SINGLETON).build()); // add the @DataSource class otherwise it won't be registered as a qualifier additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(DataSource.class).build()); - // make source datasources are initialized at startup + // make sure datasources are initialized at startup additionalBeans.produce(new AdditionalBeanBuildItem(AgroalDataSourcesInitializer.class)); // make AgroalPoolInterceptor beans unremovable, users still have to make them beans diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java index bff38eee02e67..341e2cca19966 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java @@ -113,7 +113,10 @@ public DataSources(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, * (which makes sense because {@code DataSource} is a {@code Singleton} bean). *

* This method is thread-safe + * + * @deprecated This method should not be used as it can very easily lead to timing issues during bean creation */ + @Deprecated public static AgroalDataSource fromName(String dataSourceName) { return Arc.container().instance(DataSources.class).get() .getDataSource(dataSourceName); diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java index 412b185a89f26..1fb06fce2b9c2 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java @@ -18,18 +18,21 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Default; +import jakarta.inject.Singleton; import org.flywaydb.core.Flyway; import org.flywaydb.core.api.Location; import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.migration.JavaMigration; import org.flywaydb.core.extensibility.Plugin; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.logging.Logger; +import io.quarkus.agroal.runtime.DataSources; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; import io.quarkus.agroal.spi.JdbcDataSourceSchemaReadyBuildItem; import io.quarkus.agroal.spi.JdbcInitialSQLGeneratorBuildItem; @@ -62,6 +65,7 @@ import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.flyway.runtime.FlywayBuildTimeConfig; +import io.quarkus.flyway.runtime.FlywayContainer; import io.quarkus.flyway.runtime.FlywayContainerProducer; import io.quarkus.flyway.runtime.FlywayRecorder; import io.quarkus.flyway.runtime.FlywayRuntimeConfig; @@ -71,6 +75,7 @@ class FlywayProcessor { private static final String CLASSPATH_APPLICATION_MIGRATIONS_PROTOCOL = "classpath"; + private static final String FLYWAY_CONTAINER_BEAN_NAME_PREFIX = "flyway_container_"; private static final String FLYWAY_BEAN_NAME_PREFIX = "flyway_"; private static final DotName JAVA_MIGRATION = DotName.createSimple(JavaMigration.class.getName()); @@ -172,8 +177,6 @@ void createBeans(FlywayRecorder recorder, // add the @FlywayDataSource class otherwise it won't be registered as a qualifier additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(FlywayDataSource.class).build()); - recorder.resetFlywayContainers(); - Collection dataSourceNames = getDataSourceNames(jdbcDataSourceBuildItems); for (String dataSourceName : dataSourceNames) { @@ -182,25 +185,62 @@ void createBeans(FlywayRecorder recorder, if (!hasMigrations) { createPossible = sqlGeneratorBuildItems.stream().anyMatch(s -> s.getDatabaseName().equals(dataSourceName)); } - SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem + + SyntheticBeanBuildItem.ExtendedBeanConfigurator flywayContainerConfigurator = SyntheticBeanBuildItem + .configure(FlywayContainer.class) + .scope(Singleton.class) + .setRuntimeInit() + .unremovable() + .addInjectionPoint(ClassType.create(DotName.createSimple(FlywayContainerProducer.class))) + .addInjectionPoint(ClassType.create(DotName.createSimple(DataSources.class))) + .createWith(recorder.flywayContainerFunction(dataSourceName, hasMigrations, createPossible)); + + AnnotationInstance flywayContainerQualifier; + + if (DataSourceUtil.isDefault(dataSourceName)) { + flywayContainerConfigurator.addQualifier(Default.class); + + // Flyway containers used to be ordered with the default database coming first. + // Some multitenant tests are relying on this order. + flywayContainerConfigurator.priority(10); + + flywayContainerQualifier = AnnotationInstance.builder(Default.class).build(); + } else { + String beanName = FLYWAY_CONTAINER_BEAN_NAME_PREFIX + dataSourceName; + flywayContainerConfigurator.name(beanName); + + flywayContainerConfigurator.addQualifier().annotation(DotNames.NAMED).addValue("value", beanName).done(); + flywayContainerConfigurator.addQualifier().annotation(FlywayDataSource.class).addValue("value", dataSourceName) + .done(); + flywayContainerConfigurator.priority(5); + + flywayContainerQualifier = AnnotationInstance.builder(FlywayDataSource.class).add("value", dataSourceName) + .build(); + } + + syntheticBeanBuildItemBuildProducer.produce(flywayContainerConfigurator.done()); + + SyntheticBeanBuildItem.ExtendedBeanConfigurator flywayConfigurator = SyntheticBeanBuildItem .configure(Flyway.class) - .scope(Dependent.class) // this is what the existing code does, but it doesn't seem reasonable + .scope(Singleton.class) .setRuntimeInit() .unremovable() - .supplier(recorder.flywaySupplier(dataSourceName, - hasMigrations, createPossible)); + .addInjectionPoint(ClassType.create(DotName.createSimple(FlywayContainer.class)), flywayContainerQualifier) + .createWith(recorder.flywayFunction(dataSourceName)); if (DataSourceUtil.isDefault(dataSourceName)) { - configurator.addQualifier(Default.class); + flywayConfigurator.addQualifier(Default.class); + flywayConfigurator.priority(10); } else { String beanName = FLYWAY_BEAN_NAME_PREFIX + dataSourceName; - configurator.name(beanName); + flywayConfigurator.name(beanName); + flywayConfigurator.priority(5); - configurator.addQualifier().annotation(DotNames.NAMED).addValue("value", beanName).done(); - configurator.addQualifier().annotation(FlywayDataSource.class).addValue("value", dataSourceName).done(); + flywayConfigurator.addQualifier().annotation(DotNames.NAMED).addValue("value", beanName).done(); + flywayConfigurator.addQualifier().annotation(FlywayDataSource.class).addValue("value", dataSourceName).done(); } - syntheticBeanBuildItemBuildProducer.produce(configurator.done()); + syntheticBeanBuildItemBuildProducer.produce(flywayConfigurator.done()); } } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java index 47537af91e9bd..11261c88c30a1 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java @@ -1,24 +1,30 @@ package io.quarkus.flyway.runtime; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; +import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.function.Supplier; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.common.runtime.DataSourceUtil; public class FlywayContainersSupplier implements Supplier> { @Override public Collection get() { - if (FlywayRecorder.FLYWAY_CONTAINERS.isEmpty()) { - return Collections.emptySet(); + List> flywayContainerHandles = Arc.container().listAll(FlywayContainer.class); + + if (flywayContainerHandles.isEmpty()) { + return Set.of(); } Set containers = new TreeSet<>(FlywayContainerComparator.INSTANCE); - containers.addAll(FlywayRecorder.FLYWAY_CONTAINERS); + for (InstanceHandle flywayContainerHandle : flywayContainerHandles) { + containers.add(flywayContainerHandle.get()); + } return containers; } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java index 94c7919fe4615..2b0f724a9a16f 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java @@ -1,13 +1,13 @@ package io.quarkus.flyway.runtime; -import java.util.ArrayList; +import java.lang.annotation.Annotation; import java.util.Collection; -import java.util.List; import java.util.Map; -import java.util.function.Supplier; +import java.util.function.Function; import javax.sql.DataSource; +import jakarta.enterprise.inject.Default; import jakarta.enterprise.inject.UnsatisfiedResolutionException; import org.flywaydb.core.Flyway; @@ -18,6 +18,10 @@ import io.quarkus.agroal.runtime.DataSources; import io.quarkus.agroal.runtime.UnconfiguredDataSource; import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.flyway.FlywayDataSource.FlywayDataSourceLiteral; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; @@ -26,8 +30,6 @@ public class FlywayRecorder { private static final Logger log = Logger.getLogger(FlywayRecorder.class); - static final List FLYWAY_CONTAINERS = new ArrayList<>(2); - private final RuntimeValue config; public FlywayRecorder(RuntimeValue config) { @@ -49,27 +51,37 @@ public void setApplicationCallbackClasses(Map> call QuarkusPathLocationScanner.setApplicationCallbackClasses(callbackClasses); } - public void resetFlywayContainers() { - FLYWAY_CONTAINERS.clear(); - } - - public Supplier flywaySupplier(String dataSourceName, boolean hasMigrations, boolean createPossible) { - DataSource dataSource = DataSources.fromName(dataSourceName); - if (dataSource instanceof UnconfiguredDataSource) { - return new Supplier() { - @Override - public Flyway get() { + public Function, FlywayContainer> flywayContainerFunction(String dataSourceName, + boolean hasMigrations, + boolean createPossible) { + return new Function<>() { + @Override + public FlywayContainer apply(SyntheticCreationalContext context) { + DataSource dataSource = context.getInjectedReference(DataSources.class).getDataSource(dataSourceName); + if (dataSource instanceof UnconfiguredDataSource) { throw new UnsatisfiedResolutionException("No datasource present"); } - }; - } - FlywayContainerProducer flywayProducer = Arc.container().instance(FlywayContainerProducer.class).get(); - FlywayContainer flywayContainer = flywayProducer.createFlyway(dataSource, dataSourceName, hasMigrations, - createPossible); - FLYWAY_CONTAINERS.add(flywayContainer); - return new Supplier() { + + FlywayContainerProducer flywayProducer = context.getInjectedReference(FlywayContainerProducer.class); + FlywayContainer flywayContainer = flywayProducer.createFlyway(dataSource, dataSourceName, hasMigrations, + createPossible); + return flywayContainer; + } + }; + } + + public Function, Flyway> flywayFunction(String dataSourceName) { + return new Function<>() { @Override - public Flyway get() { + public Flyway apply(SyntheticCreationalContext context) { + Annotation flywayContainerQualifier; + if (DataSourceUtil.isDefault(dataSourceName)) { + flywayContainerQualifier = Default.Literal.INSTANCE; + } else { + flywayContainerQualifier = FlywayDataSourceLiteral.of(dataSourceName); + } + + FlywayContainer flywayContainer = context.getInjectedReference(FlywayContainer.class, flywayContainerQualifier); return flywayContainer.getFlyway(); } }; @@ -79,7 +91,10 @@ public void doStartActions() { if (!config.getValue().enabled) { return; } - for (FlywayContainer flywayContainer : FLYWAY_CONTAINERS) { + + for (InstanceHandle flywayContainerHandle : Arc.container().listAll(FlywayContainer.class)) { + FlywayContainer flywayContainer = flywayContainerHandle.get(); + if (flywayContainer.isCleanAtStart()) { flywayContainer.getFlyway().clean(); } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java index b5026a133c9ea..57dab412c16d1 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java @@ -1,23 +1,25 @@ package io.quarkus.flyway.runtime; +import io.quarkus.arc.Arc; import io.quarkus.datasource.runtime.DatabaseSchemaProvider; public class FlywaySchemaProvider implements DatabaseSchemaProvider { + @Override public void resetDatabase(String dbName) { - for (FlywayContainer i : FlywayRecorder.FLYWAY_CONTAINERS) { - if (i.getDataSourceName().equals(dbName)) { - i.getFlyway().clean(); - i.getFlyway().migrate(); + for (FlywayContainer flywayContainer : Arc.container().select(FlywayContainer.class)) { + if (flywayContainer.getDataSourceName().equals(dbName)) { + flywayContainer.getFlyway().clean(); + flywayContainer.getFlyway().migrate(); } } } @Override public void resetAllDatabases() { - for (FlywayContainer i : FlywayRecorder.FLYWAY_CONTAINERS) { - i.getFlyway().clean(); - i.getFlyway().migrate(); + for (FlywayContainer flywayContainer : Arc.container().select(FlywayContainer.class)) { + flywayContainer.getFlyway().clean(); + flywayContainer.getFlyway().migrate(); } } } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/config/DialectVersions.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/config/DialectVersions.java index 43916e2285fe9..827437c97de90 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/config/DialectVersions.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/config/DialectVersions.java @@ -22,7 +22,7 @@ public static final class Defaults { public static final String ORACLE = "12"; // This must be aligned on the H2 version in the Quarkus BOM - public static final String H2 = "2.1.214"; + public static final String H2 = "2.2.220"; private Defaults() { } diff --git a/extensions/jdbc/jdbc-h2/deployment/src/main/java/io/quarkus/jdbc/h2/deployment/JDBCH2Processor.java b/extensions/jdbc/jdbc-h2/deployment/src/main/java/io/quarkus/jdbc/h2/deployment/JDBCH2Processor.java index f83906ba47abd..1f44be5e5f393 100644 --- a/extensions/jdbc/jdbc-h2/deployment/src/main/java/io/quarkus/jdbc/h2/deployment/JDBCH2Processor.java +++ b/extensions/jdbc/jdbc-h2/deployment/src/main/java/io/quarkus/jdbc/h2/deployment/JDBCH2Processor.java @@ -1,5 +1,7 @@ package io.quarkus.jdbc.h2.deployment; +import java.util.Set; + import io.quarkus.agroal.spi.JdbcDriverBuildItem; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.processor.BuiltinScope; @@ -12,10 +14,12 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.RemovedResourceBuildItem; import io.quarkus.deployment.builditem.SslNativeConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageEnableModule; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.jdbc.h2.runtime.H2AgroalConnectionConfigurer; +import io.quarkus.maven.dependency.ArtifactKey; public class JDBCH2Processor { @@ -62,4 +66,10 @@ NativeImageEnableModule registerNetModuleForNative() { //Compiling H2 to native requires activating the jdk.net module of the JDK return new NativeImageEnableModule("jdk.net"); } + + @BuildStep + void excludeNativeImageDirectives(BuildProducer removedResources) { + removedResources.produce(new RemovedResourceBuildItem(ArtifactKey.fromString("com.h2database:h2"), + Set.of("META-INF/native-image/reflect-config.json"))); + } } diff --git a/extensions/jdbc/jdbc-h2/runtime/src/main/java/io/quarkus/jdbc/h2/runtime/graalvm/DeleteFullTextLucene.java b/extensions/jdbc/jdbc-h2/runtime/src/main/java/io/quarkus/jdbc/h2/runtime/graalvm/DeleteFullTextLucene.java new file mode 100644 index 0000000000000..c5f16883dd5d6 --- /dev/null +++ b/extensions/jdbc/jdbc-h2/runtime/src/main/java/io/quarkus/jdbc/h2/runtime/graalvm/DeleteFullTextLucene.java @@ -0,0 +1,10 @@ +package io.quarkus.jdbc.h2.runtime.graalvm; + +import com.oracle.svm.core.annotate.Delete; +import com.oracle.svm.core.annotate.TargetClass; + +@Delete +@TargetClass(className = "org.h2.fulltext.FullTextLucene") +public final class DeleteFullTextLucene { + +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java index 47927799855d8..58fb10d804e16 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java @@ -16,6 +16,7 @@ import io.quarkus.kubernetes.spi.KubernetesJobBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; import io.quarkus.kubernetes.spi.PolicyRule; public class InitTaskProcessor { @@ -34,6 +35,7 @@ static void process( BuildProducer env, BuildProducer roles, BuildProducer roleBindings, + BuildProducer serviceAccount, BuildProducer decorators) { boolean generateRoleForJobs = false; @@ -73,6 +75,7 @@ static void process( List.of("get"))), target)); roleBindings.produce(new KubernetesRoleBindingBuildItem(null, "view-jobs", false, target)); + serviceAccount.produce(new KubernetesServiceAccountBuildItem(true)); } } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java index 78bc5a2f8b4f9..511a1d5f8f1e8 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java @@ -692,8 +692,24 @@ public static List createInitJobDecorators(String target, St .map(Optional::get) .collect(Collectors.toList()); + List imagePullSecretDecorators = decorators.stream() + .filter(d -> d.getGroup() == null || d.getGroup().equals(target)) + .map(d -> d.getDecorator(AddImagePullSecretDecorator.class)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + items.stream().filter(item -> item.getTarget() == null || item.getTarget().equals(target)).forEach(item -> { + for (final AddImagePullSecretDecorator delegate : imagePullSecretDecorators) { + result.add(new DecoratorBuildItem(target, new NamedResourceDecorator("Job", item.getName()) { + @Override + public void andThenVisit(PodSpecBuilder builder, ObjectMeta meta) { + delegate.andThenVisit(builder, meta); + } + })); + } + result.add(new DecoratorBuildItem(target, new NamedResourceDecorator("Job", item.getName()) { @Override public void andThenVisit(ContainerBuilder builder, ObjectMeta meta) { diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java index ffcbf904b52f7..7c6f38b8697fc 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java @@ -402,11 +402,12 @@ void externalizeInitTasks( BuildProducer env, BuildProducer roles, BuildProducer roleBindings, + BuildProducer serviceAccount, BuildProducer decorators) { final String name = ResourceNameUtil.getResourceName(config, applicationInfo); if (config.externalizeInit) { InitTaskProcessor.process(OPENSHIFT, name, image, initTasks, config.initTasks, - jobs, initContainers, env, roles, roleBindings, decorators); + jobs, initContainers, env, roles, roleBindings, serviceAccount, decorators); } } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java index 3e35fbaac5fc6..1e1e904a25acd 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java @@ -314,11 +314,13 @@ void externalizeInitTasks( BuildProducer env, BuildProducer roles, BuildProducer roleBindings, + BuildProducer serviceAccount, + BuildProducer decorators) { final String name = ResourceNameUtil.getResourceName(config, applicationInfo); if (config.externalizeInit) { InitTaskProcessor.process(KUBERNETES, name, image, initTasks, config.initTasks, - jobs, initContainers, env, roles, roleBindings, decorators); + jobs, initContainers, env, roles, roleBindings, serviceAccount, decorators); } } } diff --git a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java index 33188df5e78e6..4d6c65e2b30f5 100644 --- a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java +++ b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java @@ -28,6 +28,7 @@ import org.jboss.jandex.DotName; import org.jboss.logging.Logger; +import io.quarkus.agroal.runtime.DataSources; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; import io.quarkus.agroal.spi.JdbcDataSourceSchemaReadyBuildItem; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -285,6 +286,7 @@ void createBeans(LiquibaseRecorder recorder, .setRuntimeInit() .unremovable() .addInjectionPoint(ClassType.create(DotName.createSimple(LiquibaseFactoryProducer.class))) + .addInjectionPoint(ClassType.create(DotName.createSimple(DataSources.class))) .createWith(recorder.liquibaseFunction(dataSourceName)); if (DataSourceUtil.isDefault(dataSourceName)) { diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java index 2b9429b73d5bf..d85785d61e35b 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java @@ -29,21 +29,16 @@ public LiquibaseRecorder(RuntimeValue config) { } public Function, LiquibaseFactory> liquibaseFunction(String dataSourceName) { - DataSource dataSource = DataSources.fromName(dataSourceName); - if (dataSource instanceof UnconfiguredDataSource) { - return new Function, LiquibaseFactory>() { - @Override - public LiquibaseFactory apply(SyntheticCreationalContext context) { - throw new UnsatisfiedResolutionException("No datasource has been configured"); - } - }; - } return new Function, LiquibaseFactory>() { @Override public LiquibaseFactory apply(SyntheticCreationalContext context) { + DataSource dataSource = context.getInjectedReference(DataSources.class).getDataSource(dataSourceName); + if (dataSource instanceof UnconfiguredDataSource) { + throw new UnsatisfiedResolutionException("No datasource has been configured"); + } + LiquibaseFactoryProducer liquibaseProducer = context.getInjectedReference(LiquibaseFactoryProducer.class); - LiquibaseFactory liquibaseFactory = liquibaseProducer.createLiquibaseFactory(dataSource, dataSourceName); - return liquibaseFactory; + return liquibaseProducer.createLiquibaseFactory(dataSource, dataSourceName); } }; } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java index c73a1cddb39ca..08ed5e00f0961 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java @@ -155,7 +155,7 @@ public void startRecoveryService(final TransactionManagerConfiguration transacti } public void handleShutdown(ShutdownContext context, TransactionManagerConfiguration transactions) { - context.addLastShutdownTask(() -> { + context.addShutdownTask(() -> { if (transactions.enableRecovery) { try { QuarkusRecoveryService.getInstance().stop(); @@ -166,6 +166,8 @@ public void handleShutdown(ShutdownContext context, TransactionManagerConfigurat QuarkusRecoveryService.getInstance().destroy(); } } + }); + context.addLastShutdownTask(() -> { TransactionReaper.terminate(false); }); } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitTest.java index a6cde6e299745..fb7fe0aba6d61 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitTest.java @@ -27,6 +27,7 @@ public class KubernetesWithFlywayInitTest { private static final String NAME = "kubernetes-with-flyway"; + private static final String IMAGE_PULL_SECRET = "my-pull-secret"; @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() @@ -34,6 +35,7 @@ public class KubernetesWithFlywayInitTest { .setApplicationName(NAME) .setApplicationVersion("0.1-SNAPSHOT") .setLogFileName("k8s.log") + .overrideConfigKey("quarkus.kubernetes.image-pull-secrets", IMAGE_PULL_SECRET) .setForcedDependencies(Arrays.asList( new AppArtifact("io.quarkus", "quarkus-kubernetes", Version.getVersion()), new AppArtifact("io.quarkus", "quarkus-flyway", Version.getVersion()))); @@ -65,6 +67,9 @@ public void assertGeneratedResources() throws IOException { assertThat(d.getSpec()).satisfies(deploymentSpec -> { assertThat(deploymentSpec.getTemplate()).satisfies(t -> { assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getImagePullSecrets()).singleElement() + .satisfies(s -> assertThat(s.getName()).isEqualTo(IMAGE_PULL_SECRET)); + assertThat(podSpec.getServiceAccountName()).isEqualTo(NAME); assertThat(podSpec.getInitContainers()).singleElement().satisfies(container -> { assertThat(container.getName()).isEqualTo("init"); assertThat(container.getImage()).isEqualTo("groundnuty/k8s-wait-for:no-root-v1.7"); @@ -86,6 +91,8 @@ public void assertGeneratedResources() throws IOException { assertThat(jobSpec.getCompletionMode()).isEqualTo("NonIndexed"); assertThat(jobSpec.getTemplate()).satisfies(t -> { assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getImagePullSecrets()).singleElement() + .satisfies(s -> assertThat(s.getName()).isEqualTo(IMAGE_PULL_SECRET)); assertThat(podSpec.getRestartPolicy()).isEqualTo("OnFailure"); assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { assertThat(container.getName()).isEqualTo(NAME + "-flyway-init"); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/PackageIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/PackageIT.java index 86e576f03b12a..2859505ab1f51 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/PackageIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/PackageIT.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -27,6 +28,7 @@ import io.quarkus.maven.it.verifier.MavenProcessInvocationResult; import io.quarkus.maven.it.verifier.RunningInvoker; +import io.quarkus.runtime.util.HashUtil; @DisableForNative public class PackageIT extends MojoTestBase { @@ -34,6 +36,42 @@ public class PackageIT extends MojoTestBase { private RunningInvoker running; private File testDir; + @Test + public void testConfigTracking() throws Exception { + testDir = initProject("projects/config-tracking"); + running = new RunningInvoker(testDir, false); + var configDump = new File(new File(testDir, ".quarkus"), "quarkus-prod-config-dump"); + var configCheck = new File(new File(testDir, "target"), "quarkus-prod-config-check"); + + // initial build that generates .quarkus/quarkus-prod-config-dump + var result = running.execute(List.of("clean package -DskipTests"), Map.of()); + assertThat(result.getProcess().waitFor()).isEqualTo(0); + assertThat(configDump).exists(); + assertThat(configCheck).doesNotExist(); + + // rebuild and compare the files + result = running.execute(List.of("package -DskipTests"), Map.of()); + assertThat(result.getProcess().waitFor()).isEqualTo(0); + assertThat(configDump).exists(); + assertThat(configCheck).exists(); + assertThat(configDump).hasSameTextualContentAs(configCheck); + + var props = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(configDump.toPath())) { + props.load(reader); + } + assertThat(props).containsEntry("quarkus.application.name", HashUtil.sha512("code-with-quarkus")); + + assertThat(props).doesNotContainKey("quarkus.platform.group-id"); + for (var name : props.stringPropertyNames()) { + assertThat(name).doesNotStartWith("quarkus.test."); + } + + result = running.execute(List.of("package -DskipTests -Dquarkus.package.type=uber-jar"), Map.of()); + assertThat(result.getProcess().waitFor()).isEqualTo(0); + assertThat(running.log()).contains("Option quarkus.package.type has changed since the last build from jar to uber-jar"); + } + @Test public void testPluginClasspathConfig() throws Exception { testDir = initProject("projects/test-plugin-classpath-config"); diff --git a/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/pom.xml new file mode 100644 index 0000000000000..434437b4d2610 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + org.acme + code-with-quarkus + 1.0.0-SNAPSHOT + + ${compiler-plugin.version} + ${maven.compiler.release} + UTF-8 + UTF-8 + quarkus-bom + io.quarkus + ${project.version} + + + + + \${quarkus.platform.group-id} + \${quarkus.platform.artifact-id} + \${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-resteasy-reactive + + + + + + \${quarkus.platform.group-id} + quarkus-maven-plugin + \${quarkus.platform.version} + true + + + track-prod-config-changes + process-resources + + track-config-changes + + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + \${compiler-plugin.version} + + + -parameters + + + + + + \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/src/main/java/org/acme/GreetingResource.java b/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/src/main/java/org/acme/GreetingResource.java new file mode 100644 index 0000000000000..6938062ec8ff7 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/src/main/java/org/acme/GreetingResource.java @@ -0,0 +1,16 @@ +package org.acme; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello from RESTEasy Reactive"; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/src/main/resources/application.properties b/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/src/main/resources/application.properties new file mode 100644 index 0000000000000..ff43978c7f6ca --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/config-tracking/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.config-tracking.enabled=true +quarkus.config-tracking.hash-options=quarkus.application.* +quarkus.config-tracking.exclude=quarkus.test.*,quarkus.platform.group-id \ No newline at end of file diff --git a/pom.xml b/pom.xml index e12a0b521db88..016defe17e9b0 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 1.2.1 3.22.0 ${protoc.version} - 2.19.1 + 2.23.0 7.4.0 diff --git a/test-framework/devmode-test-utils/src/main/java/io/quarkus/test/devmode/util/DevModeTestUtils.java b/test-framework/devmode-test-utils/src/main/java/io/quarkus/test/devmode/util/DevModeTestUtils.java new file mode 100644 index 0000000000000..8d2050fa26e34 --- /dev/null +++ b/test-framework/devmode-test-utils/src/main/java/io/quarkus/test/devmode/util/DevModeTestUtils.java @@ -0,0 +1,91 @@ +package io.quarkus.test.devmode.util; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * @deprecated Use {@link DevModeClient} instead (the methods on that class are non-static to allow ports to be specified). + */ +@Deprecated(since = "3.3", forRemoval = true) +public class DevModeTestUtils { + + private static final DevModeClient devModeClient = new DevModeClient(); + + public static List killDescendingProcesses() { + return DevModeClient.killDescendingProcesses(); + } + + public static void filter(File input, Map variables) throws IOException { + DevModeClient.filter(input, variables); + } + + public static void awaitUntilServerDown() { + devModeClient.awaitUntilServerDown(); + } + + public static String getHttpResponse() { + return devModeClient.getHttpResponse(); + } + + public static String getHttpResponse(Supplier brokenReason) { + return devModeClient.getHttpResponse(brokenReason); + } + + public static String getHttpErrorResponse() { + return devModeClient.getHttpErrorResponse(); + } + + public static String getHttpErrorResponse(Supplier brokenReason) { + return devModeClient.getHttpErrorResponse(brokenReason); + } + + public static String getHttpResponse(String path) { + return devModeClient.getHttpResponse(path); + } + + public static String getHttpResponse(String path, Supplier brokenReason) { + return devModeClient.getHttpResponse(path, brokenReason); + } + + public static String getHttpResponse(String path, boolean allowError) { + return devModeClient.getHttpResponse(path, allowError); + } + + public static String getHttpResponse(String path, boolean allowError, Supplier brokenReason) { + return devModeClient.getHttpResponse(path, allowError, brokenReason); + } + + public static String getHttpResponse(String path, boolean allowError, Supplier brokenReason, long timeout, + TimeUnit tu) { + return devModeClient.getHttpResponse(path, allowError, brokenReason, timeout, tu); + } + + public static boolean getHttpResponse(String path, int expectedStatus) { + return devModeClient.getHttpResponse(path, expectedStatus); + } + + public static boolean getHttpResponse(String path, int expectedStatus, long timeout, TimeUnit tu) { + return devModeClient.getHttpResponse(path, expectedStatus, timeout, tu); + } + + // will fail if it receives any http response except the expected one + public static boolean getStrictHttpResponse(String path, int expectedStatus) { + return devModeClient.getStrictHttpResponse(path, expectedStatus); + } + + public static String get() throws IOException { + return devModeClient.get(); + } + + public static String get(String urlStr) throws IOException { + return devModeClient.get(urlStr); + } + + public static boolean isCode(String path, int code) { + return devModeClient.isCode(path, code); + } +}