diff --git a/adr/0007-quarkus-workshop-structure.adoc b/adr/0007-quarkus-workshop-structure.adoc new file mode 100644 index 00000000000000..9bfd794156568a --- /dev/null +++ b/adr/0007-quarkus-workshop-structure.adoc @@ -0,0 +1,81 @@ += Structure for Quarkus Workshops + +* Status: _Accepted_ +* Date: 2024-11-06 +* Authors: @cescoffier + +== Context and Problem Statement + +Since the public release of Quarkus, we launched a hands-on workshop to help developers get started with it. +Known as the "Quarkus Superheroes" workshop, this workshop allowed developers to learn Quarkus by actively writing and running code in a structured environment, often at conferences or in classroom settings. + +The "Quarkus Superheroes" workshop has been highly successful, delivered at various conferences and widely used by developers for self-study. +At the time, we anticipated additional workshops, leading us to establish a dedicated structure within a single repository: Quarkus Workshops (https://github.com/quarkusio/quarkus-workshops). + +The initial (and still existing) structure was straightforward: + +[source] +---- +. +├── README.md +└── quarkus-workshop-super-heroes/ + ├── dist + ├── docs + ├── super-heroes + ├── README.adoc + └── pom.xml +---- + +Although this structure was meant to support multiple workshops, only the "Quarkus Superheroes" workshop was added. +Instead of separate workshops, we expanded this initial workshop with additional steps and features. + +As we now develop new workshops on various topics, we face limitations with the single repository structure. +For example, the Quarkus LangChain4J workshop was created separately to demonstrate Quarkus LangChain4J usage, yet it isn’t integrated into the main workshop repository. +Additionally, having a single repository complicates using GitHub Pages for documentation. + +Given the current and future workshops, it’s essential to reconsider the structure to allow easier management and discoverability of each workshop. + +== Proposed New Structure + +Our experience shows that hosting all workshops in one repository isn’t optimal. We propose a new structure as follows: + +1. Each workshop will be hosted in its own repository. +This simplifies management, avoids conflicts in `README` and documentation setup, and improves workshop discoverability. +2. Naming convention: Each workshop repository should follow the format `quarkus-workshop-`, where `` represents the workshop subject (e.g., `quarkus-workshop-superheroes`, `quarkus-workshop-langchain4j`). +3. Documentation should be hosted with GitHub Pages in each repository, making each workshop more accessible. +4. Each workshop repository should have the `workshop` topic to facilitate discoverability. +5. We will keep https://quarkus.io/quarkus-workshops/ as a landing page, which people can use to find workshops. +In order to preserve the GitHub history, the quarkus-workshops repository should be renamed to https://quarkus.io/quarkus-workshop-superheroes, and then a new repository should be created, using the old name, `quarkus-workshops`. +6. This _landing_ repository can also be used to host redirects. For example, the existing URL https://quarkus.io/quarkus-workshops/super-heroes/ should be kept valid by using a redirect. + +== Considered Options + +=== Option 1: Continue with the current single-repository approach + +This would mean keeping all workshops under the existing repository. +However, as observed, this approach has not met expectations and makes workshop management more challenging. + +=== Option 2: Create a separate organization for workshops + +A dedicated organization could host all workshops, offering a single access point. +However, this approach could reduce discoverability, but would not use the Quarkus organization’s CI resources. +CI resource usage is minor, as workshops are not frequently updated. + +== Consequences + +=== Positive + +* Simplified workshop management. +* Greater autonomy for workshop maintainers. +* Consolidation of workshops previously hosted in separate repositories. + +=== Negative + +* Lack of a central place to list all workshops. This could be mitigated by creating a dedicated page on the Quarkus website. +* Potential CI resource shortage as each workshop repository uses _quarkusio_ organization CI resources. +However, this is unlikely to be a significant issue, as, generally, workshops don't use much CI resources. +That being said, it would require monitoring to ensure it doesn't become a problem. + +=== Neutral + +* Existing workshops would need restructuring to align with the new approach, especially the Quarkus Superheroes workshop. diff --git a/bom/application/pom.xml b/bom/application/pom.xml index b710b0b8ce0a05..aa01958f1765b8 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -158,7 +158,7 @@ 3.2.0 4.2.2 3.0.6.Final - 10.20.1 + 10.21.0 3.0.4 4.29.1 @@ -206,7 +206,7 @@ 7.0.0.202409031743-r 0.15.0 - 9.45 + 9.46 0.9.6 0.0.12 0.1.3 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 21755e0b2979f6..bd780c85a57ff3 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -598,7 +598,7 @@ org.apache.groovy groovy - 4.0.23 + 4.0.24 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogSocketFormatBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogSocketFormatBuildItem.java new file mode 100644 index 00000000000000..23aeaf109b9553 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogSocketFormatBuildItem.java @@ -0,0 +1,36 @@ +package io.quarkus.deployment.builditem; + +import java.util.Optional; +import java.util.logging.Formatter; + +import org.wildfly.common.Assert; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * The socket format build item. Producing this item will cause the logging subsystem to disregard its + * socket logging formatting configuration and use the formatter provided instead. If multiple formatters + * are enabled at runtime, a warning message is printed and only one is used. + */ +public final class LogSocketFormatBuildItem extends MultiBuildItem { + private final RuntimeValue> formatterValue; + + /** + * Construct a new instance. + * + * @param formatterValue the optional formatter runtime value to use (must not be {@code null}) + */ + public LogSocketFormatBuildItem(final RuntimeValue> formatterValue) { + this.formatterValue = Assert.checkNotNullParam("formatterValue", formatterValue); + } + + /** + * Get the formatter value. + * + * @return the formatter value + */ + public RuntimeValue> getFormatterValue() { + return formatterValue; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index a94f8fdc3894f5..8507399dea96cb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -75,6 +75,7 @@ import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; import io.quarkus.deployment.builditem.LogFileFormatBuildItem; import io.quarkus.deployment.builditem.LogHandlerBuildItem; +import io.quarkus.deployment.builditem.LogSocketFormatBuildItem; import io.quarkus.deployment.builditem.LogSyslogFormatBuildItem; import io.quarkus.deployment.builditem.NamedLogHandlersBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; @@ -249,6 +250,7 @@ LoggingSetupBuildItem setupLoggingRuntimeInit( final List consoleFormatItems, final List fileFormatItems, final List syslogFormatItems, + final List socketFormatItems, final Optional possibleBannerBuildItem, final List logStreamBuildItems, final BuildProducer shutdownListenerBuildItemBuildProducer, @@ -290,6 +292,8 @@ LoggingSetupBuildItem setupLoggingRuntimeInit( .map(LogFileFormatBuildItem::getFormatterValue).collect(Collectors.toList()); List>> possibleSyslogFormatters = syslogFormatItems.stream() .map(LogSyslogFormatBuildItem::getFormatterValue).collect(Collectors.toList()); + List>> possibleSocketFormatters = socketFormatItems.stream() + .map(LogSocketFormatBuildItem::getFormatterValue).collect(Collectors.toList()); context.registerSubstitution(InheritableLevel.ActualLevel.class, String.class, InheritableLevel.Substitution.class); context.registerSubstitution(InheritableLevel.Inherited.class, String.class, InheritableLevel.Substitution.class); @@ -308,6 +312,7 @@ LoggingSetupBuildItem setupLoggingRuntimeInit( categoryMinLevelDefaults.content, alwaysEnableLogStream, streamingDevUiLogHandler, handlers, namedHandlers, possibleConsoleFormatters, possibleFileFormatters, possibleSyslogFormatters, + possibleSocketFormatters, possibleSupplier, launchModeBuildItem.getLaunchMode(), true))); List additionalLogCleanupFilters = new ArrayList<>(logCleanupFilters.size()); diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java index 9e252dcb0fb333..50f315897cd652 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java @@ -305,6 +305,7 @@ private BuildResult runAugment(boolean firstRun, Set changedResources, .setTargetDir(quarkusBootstrap.getTargetDirectory()) .setDeploymentClassLoader(deploymentClassLoader) .setBuildSystemProperties(quarkusBootstrap.getBuildSystemProperties()) + .setRuntimeProperties(quarkusBootstrap.getRuntimeProperties()) .setEffectiveModel(curatedApplication.getApplicationModel()) .setDependencyInfoProvider(quarkusBootstrap.getDependencyInfoProvider()); if (quarkusBootstrap.getBaseName() != null) { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java index c224ba30c764b4..646d0e5cc345dd 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java @@ -21,7 +21,7 @@ import org.jboss.jdeparser.JDeparser; import io.quarkus.annotation.processor.documentation.config.ConfigDocExtensionProcessor; -import io.quarkus.annotation.processor.documentation.config.model.Extension; +import io.quarkus.annotation.processor.documentation.config.model.ExtensionModule; import io.quarkus.annotation.processor.documentation.config.util.Types; import io.quarkus.annotation.processor.extension.ExtensionBuildProcessor; import io.quarkus.annotation.processor.util.Config; @@ -45,11 +45,12 @@ public synchronized void init(ProcessingEnvironment processingEnv) { .parseBoolean(utils.processingEnv().getOptions().getOrDefault(Options.LEGACY_CONFIG_ROOT, "false")); boolean debug = Boolean.getBoolean(DEBUG); - Extension extension = utils.extension().getExtension(); - Config config = new Config(extension, useConfigMapping, debug); + ExtensionModule extensionModule = utils.extension().getExtensionModule(); + + Config config = new Config(extensionModule, useConfigMapping, debug); if (!useConfigMapping) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Extension " + extension.artifactId() + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Extension module " + extensionModule.artifactId() + " config implementation is deprecated. Please migrate to use @ConfigMapping: https://quarkus.io/guides/writing-extensions#configuration"); } @@ -61,7 +62,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) { // for now, we generate the old config doc by default but we will change this behavior soon if (generateDoc) { - if (extension.detected()) { + if (extensionModule.detected()) { extensionProcessors.add(new ConfigDocExtensionProcessor()); } else { processingEnv.getMessager().printMessage(Kind.WARNING, diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ExtensionModule.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ExtensionModule.java new file mode 100644 index 00000000000000..09f0b3150af6b6 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ExtensionModule.java @@ -0,0 +1,20 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +public record ExtensionModule(String groupId, String artifactId, ExtensionModuleType type, Extension extension, + boolean detected) { + + public static ExtensionModule createNotDetected() { + return new ExtensionModule("not.detected", "not.detected", ExtensionModuleType.UNKNOWN, Extension.createNotDetected(), + false); + } + + public static ExtensionModule of(String groupId, String artifactId, ExtensionModuleType type, Extension extension) { + return new ExtensionModule(groupId, artifactId, type, extension, true); + } + + public enum ExtensionModuleType { + RUNTIME, + DEPLOYMENT, + UNKNOWN; + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/Config.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/Config.java index 85643f62c24eba..8440155cca060d 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/util/Config.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/Config.java @@ -1,21 +1,26 @@ package io.quarkus.annotation.processor.util; import io.quarkus.annotation.processor.documentation.config.model.Extension; +import io.quarkus.annotation.processor.documentation.config.model.ExtensionModule; public class Config { - private final Extension extension; + private final ExtensionModule extensionModule; private final boolean useConfigMapping; private final boolean debug; - public Config(Extension extension, boolean useConfigMapping, boolean debug) { - this.extension = extension; + public Config(ExtensionModule extensionModule, boolean useConfigMapping, boolean debug) { + this.extensionModule = extensionModule; this.useConfigMapping = useConfigMapping; this.debug = debug; } + public ExtensionModule getExtensionModule() { + return extensionModule; + } + public Extension getExtension() { - return extension; + return extensionModule.extension(); } public boolean useConfigMapping() { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/ExtensionUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ExtensionUtil.java index f1d6f434c8855a..81b203a1085f8a 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/util/ExtensionUtil.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ExtensionUtil.java @@ -20,10 +20,12 @@ import io.quarkus.annotation.processor.documentation.config.model.Extension; import io.quarkus.annotation.processor.documentation.config.model.Extension.NameSource; +import io.quarkus.annotation.processor.documentation.config.model.ExtensionModule; +import io.quarkus.annotation.processor.documentation.config.model.ExtensionModule.ExtensionModuleType; public final class ExtensionUtil { - private static final String RUNTIME_MARKER_FILE = "META-INF/quarkus.properties"; + private static final String RUNTIME_MARKER_FILE = "META-INF/quarkus-extension.properties"; private static final String ARTIFACT_DEPLOYMENT_SUFFIX = "-deployment"; private static final String NAME_QUARKUS_PREFIX = "Quarkus - "; @@ -42,11 +44,11 @@ public final class ExtensionUtil { * This is not exactly pretty but it's actually not easy to get the artifact id of the current artifact. * One option would be to pass it through the annotation processor but it's not exactly ideal. */ - public Extension getExtension() { + public ExtensionModule getExtensionModule() { Optional pom = filerUtil.getPomPath(); if (pom.isEmpty()) { - return Extension.createNotDetected(); + return ExtensionModule.createNotDetected(); } Document doc; @@ -61,10 +63,10 @@ public Extension getExtension() { throw new IllegalStateException("Unable to parse pom file: " + pom, e); } - return getExtensionFromPom(pom.get(), doc); + return getExtensionModuleFromPom(pom.get(), doc); } - private Extension getExtensionFromPom(Path pom, Document doc) { + private ExtensionModule getExtensionModuleFromPom(Path pom, Document doc) { String parentGroupId = null; String artifactId = null; String groupId = null; @@ -111,40 +113,45 @@ private Extension getExtensionFromPom(Path pom, Document doc) { if (groupId == null || groupId.isBlank() || artifactId == null || artifactId.isBlank()) { processingEnv.getMessager().printMessage(Kind.WARNING, "Unable to determine artifact coordinates from: " + pom); - return Extension.createNotDetected(); + return ExtensionModule.createNotDetected(); } - boolean runtime = isRuntime(); + ExtensionModuleType moduleType = detectExtensionModuleType(artifactId); - if (!runtime && artifactId.endsWith(ARTIFACT_DEPLOYMENT_SUFFIX)) { - artifactId = artifactId.substring(0, artifactId.length() - ARTIFACT_DEPLOYMENT_SUFFIX.length()); + String extensionArtifactId; + if (moduleType == ExtensionModuleType.DEPLOYMENT) { + extensionArtifactId = artifactId.substring(0, artifactId.length() - ARTIFACT_DEPLOYMENT_SUFFIX.length()); + } else { + extensionArtifactId = artifactId; } - NameSource nameSource; + NameSource extensionNameSource; Optional extensionMetadata = getExtensionMetadata(); if (extensionMetadata.isPresent()) { name = extensionMetadata.get().name(); - nameSource = NameSource.EXTENSION_METADATA; + extensionNameSource = NameSource.EXTENSION_METADATA; guideUrl = extensionMetadata.get().guideUrl(); } else if (name != null) { - nameSource = NameSource.POM_XML; + extensionNameSource = NameSource.POM_XML; } else { - nameSource = NameSource.NONE; + extensionNameSource = NameSource.NONE; } - if (name != null) { - if (name.startsWith(NAME_QUARKUS_PREFIX)) { - name = name.substring(NAME_QUARKUS_PREFIX.length()).trim(); + String extensionName = name; + if (extensionName != null) { + if (extensionName.startsWith(NAME_QUARKUS_PREFIX)) { + extensionName = extensionName.substring(NAME_QUARKUS_PREFIX.length()).trim(); } - if (!runtime && name.endsWith(NAME_DEPLOYMENT_SUFFIX)) { - name = name.substring(0, name.length() - NAME_DEPLOYMENT_SUFFIX.length()); + if (moduleType == ExtensionModuleType.DEPLOYMENT && extensionName.endsWith(NAME_DEPLOYMENT_SUFFIX)) { + extensionName = extensionName.substring(0, extensionName.length() - NAME_DEPLOYMENT_SUFFIX.length()); } - if (runtime && name.endsWith(NAME_RUNTIME_SUFFIX)) { - name = name.substring(0, name.length() - NAME_RUNTIME_SUFFIX.length()); + if (moduleType == ExtensionModuleType.RUNTIME && extensionName.endsWith(NAME_RUNTIME_SUFFIX)) { + extensionName = extensionName.substring(0, extensionName.length() - NAME_RUNTIME_SUFFIX.length()); } } - return Extension.of(groupId, artifactId, name, nameSource, guideUrl); + return ExtensionModule.of(groupId, artifactId, moduleType, + Extension.of(groupId, extensionArtifactId, extensionName, extensionNameSource, guideUrl)); } private Optional getExtensionMetadata() { @@ -179,13 +186,21 @@ private Optional getExtensionMetadata() { private record ExtensionMetadata(String name, String guideUrl) { } - private boolean isRuntime() { + private ExtensionModuleType detectExtensionModuleType(String artifactId) { try { Path runtimeMarkerFile = Paths .get(processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", RUNTIME_MARKER_FILE).toUri()); - return Files.exists(runtimeMarkerFile); + if (Files.exists(runtimeMarkerFile)) { + return ExtensionModuleType.RUNTIME; + } } catch (IOException e) { - return false; + // ignore, the file doesn't exist } + + if (artifactId.endsWith(ARTIFACT_DEPLOYMENT_SUFFIX)) { + return ExtensionModuleType.DEPLOYMENT; + } + + return ExtensionModuleType.UNKNOWN; } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogRuntimeConfig.java index ed01e44255ffa6..28a2516ae0a237 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogRuntimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogRuntimeConfig.java @@ -10,6 +10,7 @@ import java.util.logging.Level; import org.jboss.logmanager.handlers.AsyncHandler.OverflowAction; +import org.jboss.logmanager.handlers.SocketHandler; import org.jboss.logmanager.handlers.SyslogHandler.Facility; import org.jboss.logmanager.handlers.SyslogHandler.Protocol; import org.jboss.logmanager.handlers.SyslogHandler.SyslogType; @@ -77,6 +78,14 @@ public interface LogRuntimeConfig { @ConfigDocSection SyslogConfig syslog(); + /** + * Socket logging. + *

+ * Logging to a socket is also supported but not enabled by default. + */ + @ConfigDocSection + SocketConfig socket(); + /** * Logging categories. *

@@ -115,6 +124,15 @@ public interface LogRuntimeConfig { @ConfigDocSection Map syslogHandlers(); + /** + * Socket handlers. + *

+ * The named socket handlers configured here can be linked to one or more categories. + */ + @WithName("handler.socket") + @ConfigDocSection + Map socketHandlers(); + /** * Log cleanup filters - internal use. */ @@ -207,6 +225,9 @@ interface FileConfig { interface RotationConfig { /** * The maximum log file size, after which a rotation is executed. + * Note that the file is rotated after the log record is written. + * Thus, this isn't a hard maximum on the file size; rather, it's a hard minimum + * on the size of the file before it is rotated. */ @WithDefault("10M") @WithConverter(MemorySizeConverter.class) @@ -393,6 +414,59 @@ interface SyslogConfig { AsyncConfig async(); } + interface SocketConfig { + + /** + * If socket logging should be enabled + */ + @WithDefault("false") + boolean enable(); + + /** + * + * The IP address and port of the server receiving the logs + */ + @WithDefault("localhost:4560") + @WithConverter(InetSocketAddressConverter.class) + InetSocketAddress endpoint(); + + /** + * Sets the protocol used to connect to the syslog server + */ + @WithDefault("tcp") + SocketHandler.Protocol protocol(); + + /** + * Enables or disables blocking when attempting to reconnect a + * {@link Protocol#TCP + * TCP} or {@link Protocol#SSL_TCP SSL TCP} protocol + */ + @WithDefault("false") + boolean blockOnReconnect(); + + /** + * The log message format + */ + @WithDefault("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n") + String format(); + + /** + * The log level specifying, which message levels will be logged by socket logger + */ + @WithDefault("ALL") + Level level(); + + /** + * The name of the filter to link to the file handler. + */ + Optional filter(); + + /** + * Socket async logging config + */ + AsyncConfig async(); + } + interface CleanupFilterConfig { /** * The message prefix to match diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 6cea3eb8178231..2d390ab877db0e 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -46,6 +46,7 @@ import org.jboss.logmanager.handlers.FileHandler; import org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler; import org.jboss.logmanager.handlers.SizeRotatingFileHandler; +import org.jboss.logmanager.handlers.SocketHandler; import org.jboss.logmanager.handlers.SyslogHandler; import io.quarkus.bootstrap.logging.InitialConfigurator; @@ -63,6 +64,7 @@ import io.quarkus.runtime.logging.LogRuntimeConfig.CleanupFilterConfig; import io.quarkus.runtime.logging.LogRuntimeConfig.ConsoleConfig; import io.quarkus.runtime.logging.LogRuntimeConfig.FileConfig; +import io.quarkus.runtime.logging.LogRuntimeConfig.SocketConfig; import io.quarkus.runtime.shutdown.ShutdownListener; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; @@ -116,7 +118,7 @@ public String getName() { new LoggingSetupRecorder(new RuntimeValue<>(consoleRuntimeConfig)).initializeLogging(logRuntimeConfig, logBuildTimeConfig, DiscoveredLogComponents.ofEmpty(), emptyMap(), false, null, emptyList(), emptyList(), emptyList(), emptyList(), - emptyList(), banner, LaunchMode.DEVELOPMENT, false); + emptyList(), emptyList(), banner, LaunchMode.DEVELOPMENT, false); } public ShutdownListener initializeLogging( @@ -131,6 +133,7 @@ public ShutdownListener initializeLogging( final List>> possibleConsoleFormatters, final List>> possibleFileFormatters, final List>> possibleSyslogFormatters, + final List>> possibleSocketFormatters, final RuntimeValue>> possibleBannerSupplier, final LaunchMode launchMode, final boolean includeFilters) { @@ -211,6 +214,14 @@ public void close() throws SecurityException { } } + if (config.socket().enable()) { + final Handler socketHandler = configureSocketHandler(config.socket(), errorManager, cleanupFiler, + namedFilters, possibleSocketFormatters, includeFilters); + if (socketHandler != null) { + handlers.add(socketHandler); + } + } + if ((launchMode.isDevOrTest() || enableWebStream) && streamingDevUiConsoleHandler != null && streamingDevUiConsoleHandler.getValue().isPresent()) { @@ -229,7 +240,7 @@ public void close() throws SecurityException { Map namedHandlers = shouldCreateNamedHandlers(config, additionalNamedHandlers) ? createNamedHandlers(config, consoleRuntimeConfig.getValue(), additionalNamedHandlers, - possibleConsoleFormatters, possibleFileFormatters, possibleSyslogFormatters, + possibleConsoleFormatters, possibleFileFormatters, possibleSyslogFormatters, possibleSocketFormatters, errorManager, cleanupFiler, namedFilters, launchMode, shutdownNotifier, includeFilters) : emptyMap(); @@ -328,7 +339,7 @@ public static void initializeBuildTimeLogging( } Map namedHandlers = createNamedHandlers(config, consoleConfig, emptyList(), - emptyList(), emptyList(), emptyList(), errorManager, logCleanupFilter, + emptyList(), emptyList(), emptyList(), emptyList(), errorManager, logCleanupFilter, emptyMap(), launchMode, dummy, false); setUpCategoryLoggers(buildConfig, categoryDefaultMinLevels, categories, logContext, errorManager, namedHandlers); @@ -388,6 +399,7 @@ private static Map createNamedHandlers( List>> possibleConsoleFormatters, List>> possibleFileFormatters, List>> possibleSyslogFormatters, + List>> possibleSocketFormatters, ErrorManager errorManager, LogCleanupFilter cleanupFilter, Map namedFilters, LaunchMode launchMode, ShutdownNotifier shutdownHandler, boolean includeFilters) { @@ -422,6 +434,17 @@ private static Map createNamedHandlers( addToNamedHandlers(namedHandlers, syslogHandler, sysLogConfigEntry.getKey()); } } + for (Entry socketConfigEntry : config.socketHandlers().entrySet()) { + SocketConfig namedSocketConfig = socketConfigEntry.getValue(); + if (!namedSocketConfig.enable()) { + continue; + } + final Handler socketHandler = configureSocketHandler(namedSocketConfig, errorManager, cleanupFilter, + namedFilters, possibleSocketFormatters, includeFilters); + if (socketHandler != null) { + addToNamedHandlers(namedHandlers, socketHandler, socketConfigEntry.getKey()); + } + } Map additionalNamedHandlersMap; if (additionalNamedHandlers.isEmpty()) { @@ -770,6 +793,53 @@ private static Handler configureSyslogHandler(final LogRuntimeConfig.SyslogConfi } } + private static Handler configureSocketHandler(final LogRuntimeConfig.SocketConfig config, + final ErrorManager errorManager, + final LogCleanupFilter logCleanupFilter, + final Map namedFilters, + final List>> possibleSocketFormatters, + final boolean includeFilters) { + try { + final SocketHandler handler = new SocketHandler(config.endpoint().getHostString(), config.endpoint().getPort()); + handler.setProtocol(config.protocol()); + handler.setBlockOnReconnect(config.blockOnReconnect()); + handler.setLevel(config.level()); + + Formatter formatter = null; + boolean formatterWarning = false; + for (RuntimeValue> value : possibleSocketFormatters) { + if (formatter != null) { + formatterWarning = true; + } + final Optional val = value.getValue(); + if (val.isPresent()) { + formatter = val.get(); + } + } + if (formatter == null) { + formatter = new PatternFormatter(config.format()); + } + handler.setFormatter(formatter); + + handler.setErrorManager(errorManager); + handler.setFilter(logCleanupFilter); + applyFilter(includeFilters, errorManager, logCleanupFilter, config.filter(), namedFilters, handler); + + if (formatterWarning) { + handler.getErrorManager().error("Multiple socket formatters were activated", null, + ErrorManager.GENERIC_FAILURE); + } + + if (config.async().enable()) { + return createAsyncHandler(config.async(), config.level(), handler); + } + return handler; + } catch (IOException e) { + errorManager.error("Failed to create socket handler", e, ErrorManager.OPEN_FAILURE); + return null; + } + } + private static AsyncHandler createAsyncHandler(LogRuntimeConfig.AsyncConfig asyncConfig, Level level, Handler handler) { final AsyncHandler asyncHandler = new AsyncHandler(asyncConfig.queueLength()); asyncHandler.setOverflowAction(asyncConfig.overflow()); diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java index 7b68c1ee47259a..ba0cf3fff9f25c 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java @@ -37,6 +37,8 @@ import io.quarkus.annotation.processor.documentation.config.model.Extension; import io.quarkus.maven.config.doc.generator.Format; import io.quarkus.maven.config.doc.generator.Formatter; +import io.quarkus.maven.config.doc.generator.GenerationReport; +import io.quarkus.maven.config.doc.generator.GenerationReport.GenerationViolation; import io.quarkus.qute.Engine; import io.quarkus.qute.ReflectionValueResolver; import io.quarkus.qute.UserTagSectionHelper; @@ -91,13 +93,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { List targetDirectories = findTargetDirectories(resolvedScanDirectory); + GenerationReport generationReport = new GenerationReport(); JavadocRepository javadocRepository = JavadocMerger.mergeJavadocElements(targetDirectories); MergedModel mergedModel = ModelMerger.mergeModel(javadocRepository, targetDirectories, true); Format normalizedFormat = Format.normalizeFormat(format); String normalizedTheme = normalizedFormat.normalizeTheme(theme); - Formatter formatter = Formatter.getFormatter(javadocRepository, enableEnumTooltips, normalizedFormat); + Formatter formatter = Formatter.getFormatter(generationReport, javadocRepository, enableEnumTooltips, normalizedFormat); Engine quteEngine = initializeQuteEngine(formatter, normalizedFormat, normalizedTheme); // we generate a file per extension + top level prefix @@ -168,6 +171,21 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } + if (!generationReport.getViolations().isEmpty()) { + StringBuilder report = new StringBuilder( + "One or more errors happened during the configuration documentation generation. Here is a full report:\n\n"); + for (Entry> violationsEntry : generationReport.getViolations().entrySet()) { + report.append("- ").append(violationsEntry.getKey()).append("\n"); + for (GenerationViolation violation : violationsEntry.getValue()) { + report.append(" . ").append(violation.sourceElement()).append(" - ").append(violation.message()) + .append("\n"); + } + report.append("\n----\n\n"); + } + + throw new IllegalStateException(report.toString()); + } + // we generate files for generated sections for (Entry> extensionConfigSectionsEntry : mergedModel.getGeneratedConfigSections() .entrySet()) { diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java index c2aca8ffb55e0c..340e2c12f3c02b 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java @@ -15,13 +15,16 @@ import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; import io.quarkus.annotation.processor.documentation.config.util.Types; import io.quarkus.maven.config.doc.GenerateConfigDocMojo.Context; +import io.quarkus.maven.config.doc.generator.GenerationReport.ConfigPropertyGenerationViolation; abstract class AbstractFormatter implements Formatter { + protected final GenerationReport generationReport; protected final JavadocRepository javadocRepository; protected final boolean enableEnumTooltips; - AbstractFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { + AbstractFormatter(GenerationReport generationReport, JavadocRepository javadocRepository, boolean enableEnumTooltips) { + this.generationReport = generationReport; this.javadocRepository = javadocRepository; this.enableEnumTooltips = enableEnumTooltips; } @@ -41,12 +44,17 @@ public String formatDescription(ConfigProperty configProperty) { configProperty.getSourceElementName()); if (javadocElement.isEmpty()) { + generationReport.addError(new ConfigPropertyGenerationViolation(configProperty.getSourceType(), + configProperty.getSourceElementName(), configProperty.getSourceElementType(), "Missing Javadoc")); return null; } String description = JavadocTransformer.transform(javadocElement.get().description(), javadocElement.get().format(), javadocFormat()); if (description == null || description.isBlank()) { + generationReport.addError(new ConfigPropertyGenerationViolation(configProperty.getSourceType(), + configProperty.getSourceElementName(), configProperty.getSourceElementType(), + "Transformed Javadoc is empty")); return null; } @@ -204,17 +212,20 @@ public String formatSectionTitle(ConfigSection configSection) { configSection.getSourceElementName()); if (javadocElement.isEmpty()) { - throw new IllegalStateException( - "Couldn't find section title for: " + configSection.getSourceType() + "#" - + configSection.getSourceElementName()); + generationReport.addError(new ConfigPropertyGenerationViolation(configSection.getSourceType(), + configSection.getSourceElementName(), configSection.getSourceElementType(), "Missing Javadoc")); + + return null; } String javadoc = JavadocTransformer.transform(javadocElement.get().description(), javadocElement.get().format(), javadocFormat()); if (javadoc == null || javadoc.isBlank()) { - throw new IllegalStateException( - "Couldn't find section title for: " + configSection.getSourceType() + "#" - + configSection.getSourceElementName()); + generationReport.addError(new ConfigPropertyGenerationViolation(configSection.getSourceType(), + configSection.getSourceElementName(), configSection.getSourceElementType(), + "Transformed Javadoc is empty")); + + return null; } return trimFinalDot(javadoc); diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java index 007a06c89efc0e..75d8b1a370ecfd 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java @@ -24,8 +24,8 @@ final class AsciidocFormatter extends AbstractFormatter { private static final Pattern ANGLE_BRACKETS_WITH_DESCRIPTION_PATTERN = Pattern.compile("<<([a-z0-9_\\-#\\.]+?),([^>]+?)>>", Pattern.CASE_INSENSITIVE); - AsciidocFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { - super(javadocRepository, enableEnumTooltips); + AsciidocFormatter(GenerationReport generationReport, JavadocRepository javadocRepository, boolean enableEnumTooltips) { + super(generationReport, javadocRepository, enableEnumTooltips); } @Override diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java index de25d1eebdb1eb..742c571c2b2e6f 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java @@ -31,12 +31,13 @@ default String formatDescription(ConfigProperty configProperty, Extension extens String formatName(Extension extension); - static Formatter getFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips, Format format) { + static Formatter getFormatter(GenerationReport generationReport, JavadocRepository javadocRepository, + boolean enableEnumTooltips, Format format) { switch (format) { case asciidoc: - return new AsciidocFormatter(javadocRepository, enableEnumTooltips); + return new AsciidocFormatter(generationReport, javadocRepository, enableEnumTooltips); case markdown: - return new MarkdownFormatter(javadocRepository); + return new MarkdownFormatter(generationReport, javadocRepository); default: throw new IllegalArgumentException("Unsupported format: " + format); } diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/GenerationReport.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/GenerationReport.java new file mode 100644 index 00000000000000..bf3894389d95d3 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/GenerationReport.java @@ -0,0 +1,39 @@ +package io.quarkus.maven.config.doc.generator; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.annotation.processor.documentation.config.model.SourceElementType; + +public class GenerationReport { + + private Map> violations = new LinkedHashMap<>(); + + void addError(GenerationViolation error) { + this.violations.computeIfAbsent(error.sourceType(), k -> new ArrayList<>()).add(error); + } + + public Map> getViolations() { + return violations; + } + + public record ConfigPropertyGenerationViolation(String sourceType, String sourceElement, + SourceElementType sourceElementType, String message) implements GenerationViolation { + + @Override + public String sourceElement() { + return sourceElement + (sourceElementType == SourceElementType.METHOD ? "()" : ""); + } + } + + public interface GenerationViolation { + + String sourceType(); + + String sourceElement(); + + String message(); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java index d5096c259317a1..cc23c399d0352c 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java @@ -9,8 +9,8 @@ final class MarkdownFormatter extends AbstractFormatter { private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "[🛈](#%s)"; - MarkdownFormatter(JavadocRepository javadocRepository) { - super(javadocRepository, false); + MarkdownFormatter(GenerationReport generationReport, JavadocRepository javadocRepository) { + super(generationReport, javadocRepository, false); } @Override diff --git a/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc b/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc index cf4dcfff15f794..8eb3b1ba481e2a 100644 --- a/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc +++ b/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc @@ -34,3 +34,11 @@ quarkus.otel.exporter.otlp.logs.endpoint=http://logs-uri:4317 // <3> <1> The endpoint for the traces exporter. <2> The endpoint for the metrics exporter. <3> The endpoint for the logs exporter. + +If you need that your spans and logs to be exported directly as they finish +(e.g. in a serverless environment / application), you can set this property to `true`. +This replaces the default batching of data. +[source,properties] +---- +quarkus.otel.simple=true +---- diff --git a/docs/src/main/asciidoc/centralized-log-management.adoc b/docs/src/main/asciidoc/centralized-log-management.adoc index 663df993637c17..4fbb13b06388c7 100644 --- a/docs/src/main/asciidoc/centralized-log-management.adoc +++ b/docs/src/main/asciidoc/centralized-log-management.adoc @@ -236,6 +236,77 @@ networks: Launch your application, you should see your logs arriving inside the Elastic Stack; you can use Kibana available at http://localhost:5601/ to access them. + +[[logstash_ecs]] +== GELF alternative: Send logs to Logstash in the ECS (Elastic Common Schema) format + +You can also send your logs to Logstash using a TCP input in the https://www.elastic.co/guide/en/ecs-logging/overview/current/intro.html[ECS] format. +To achieve this we will use the `quarkus-logging-json` extension to format the logs in JSON format and the socket handler to send them to Logstash. + +For this you can use the same `docker-compose.yml` file as above but with a different Logstash pipeline configuration. + +[source] +---- +input { + tcp { + port => 4560 + coded => json + } +} + +filter { + if ![span][id] and [mdc][spanId] { + mutate { rename => { "[mdc][spanId]" => "[span][id]" } } + } + if ![trace][id] and [mdc][traceId] { + mutate { rename => {"[mdc][traceId]" => "[trace][id]"} } + } +} + +output { + stdout {} + elasticsearch { + hosts => ["http://elasticsearch:9200"] + } +} +---- + +Then configure your application to log in JSON format instead of GELF + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-logging-json + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-logging-json") +---- + +and specify the host and port of your Logstash endpoint. To be ECS compliant, specify the log format. + +[source, properties] +---- +# to keep the logs in the usual format in the console +quarkus.log.console.json=false + +quarkus.log.socket.enable=true +quarkus.log.socket.json=true +quarkus.log.socket.endpoint=localhost:4560 + +# to have the exception serialized into a single text element +quarkus.log.socket.json.exception-output-type=formatted + +# specify the format of the produced JSON log +quarkus.log.socket.json.log-format=ECS +---- + + == Send logs to Fluentd (EFK) First, you need to create a Fluentd image with the needed plugins: elasticsearch and input-gelf. @@ -422,6 +493,7 @@ quarkus.log.syslog.hostname=quarkus-test Launch your application, you should see your logs arriving inside EFK: you can use Kibana available at http://localhost:5601/ to access them. + == Elasticsearch indexing consideration Be careful that, by default, Elasticsearch will automatically map unknown fields (if not disabled in the index settings) by detecting their type. diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 40f358744db22f..8ab61ce549e687 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -716,6 +716,27 @@ spec: ip: 10.0.0.0 ---- +=== Add nodeSelector +To add a nodeSelector in the generated `Deployment` (more information can be found in https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes[Kubernetes documentation]), just apply the following configuration: + +[source,properties] +---- +quarkus.kubernetes.node-selector.key=diskType +quarkus.kubernetes.node-selector.value=ssd +---- + +This would generate the following `nodeSelector` section in the `deployment` definition: + +[source,yaml] +---- +kind: Deployment +spec: + template: + spec: + nodeSelector: + diskType: ssd +---- + === Container Resources Management CPU & Memory limits and requests can be applied to a `Container` (more info in https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/[Kubernetes documentation]) using the following configuration: diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index aa47489dade9a5..8d1491466bbde3 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -511,6 +511,24 @@ quarkus.log.category."com.example".use-parent-handlers=false For details about its configuration, see the xref:#quarkus-core_section_quarkus-log-syslog[Syslog logging configuration] reference. +=== Socket log handler + +This handler will send the logs to a socket. +It is disabled by default, so you must first enable it. +When enabled, it sends all log events to a socket, for instance to a Logstash server. + +This will typically be used in conjunction with the `quarkus-logging-json` extension so send logs in ECS format to an Elasticsearch instance. +An example configuration can be found in the xref:centralized-log-management.adoc[Centralized log management] guide. + +* A global configuration example: ++ +[source, properties] +---- +quarkus.log.socket.enable=true +quarkus.log.socket.endpoint=localhost:4560 +---- + + == Add a logging filter to your log handler Log handlers, such as the console log handler, can be linked with a link:https://docs.oracle.com/en/java/javase/17/docs/api/java.logging/java/util/logging/Filter.html[filter] that determines whether a log record should be logged. diff --git a/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java index 426240ae842ed6..e3ff5f25f49844 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java @@ -6,7 +6,9 @@ import io.quarkus.runtime.annotations.ConfigDocDefault; import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigGroup; +@ConfigGroup public interface CommonConfig { /** * Path to the JVM Dockerfile. diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java index 5652b15cc1ff93..77bdc8c63d43bb 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java @@ -140,7 +140,7 @@ public interface KeycloakDevServicesConfig { boolean createRealm(); /** - * Specifies whether to create the default client id `quarkus-app` with a secret `secret`and register them + * Specifies whether to create the default client id `quarkus-app` with a secret `secret` and register them * if the {@link #createRealm} property is set to true. * For OIDC extension configuration properties `quarkus.oidc.client.id` and `quarkus.oidc.credentials.secret` will * be configured. diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigurator.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigurator.java index a06f5df1a14b91..eff7e67bea5291 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigurator.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigurator.java @@ -6,7 +6,8 @@ public interface KeycloakDevServicesConfigurator { - record ConfigPropertiesContext(String authServerInternalUrl, String oidcClientId, String oidcClientSecret) { + record ConfigPropertiesContext(String authServerInternalUrl, String oidcClientId, String oidcClientSecret, + String authServerInternalBaseUrl) { } Map createProperties(ConfigPropertiesContext context); diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java index f366769ad048bb..6663e45f3e8b4b 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -287,14 +287,22 @@ private static void closeDevService() { capturedDevServicesConfiguration = null; } + private static String getBaseURL(String scheme, String host, Integer port) { + return scheme + host + ":" + port; + } + private static String startURL(String scheme, String host, Integer port, boolean isKeycloakX) { - return scheme + host + ":" + port + (isKeycloakX ? "" : "/auth"); + return getBaseURL(scheme, host, port) + (isKeycloakX ? "" : "/auth"); + } + + private static String startURL(String baseUrl, boolean isKeycloakX) { + return baseUrl + (isKeycloakX ? "" : "/auth"); } private static Map prepareConfiguration( BuildProducer keycloakBuildItemBuildProducer, String internalURL, String hostURL, List realmReps, List errors, - KeycloakDevServicesConfigurator devServicesConfigurator) { + KeycloakDevServicesConfigurator devServicesConfigurator, String internalBaseUrl) { final String realmName = realmReps != null && !realmReps.isEmpty() ? realmReps.iterator().next().getRealm() : getDefaultRealmName(); final String authServerInternalUrl = realmsURL(internalURL, realmName); @@ -338,7 +346,8 @@ private static Map prepareConfiguration( } Map configProperties = new HashMap<>(); - var configPropertiesContext = new ConfigPropertiesContext(authServerInternalUrl, oidcClientId, oidcClientSecret); + var configPropertiesContext = new ConfigPropertiesContext(authServerInternalUrl, oidcClientId, oidcClientSecret, + internalBaseUrl); configProperties.putAll(devServicesConfigurator.createProperties(configPropertiesContext)); configProperties.put(KEYCLOAK_URL_KEY, internalURL); configProperties.put(CLIENT_AUTH_SERVER_URL_CONFIG_KEY, clientAuthServerUrl); @@ -397,8 +406,9 @@ private static RunningDevService startContainer( oidcContainer.withEnv(capturedDevServicesConfiguration.containerEnv()); oidcContainer.start(); - String internalUrl = startURL((oidcContainer.isHttps() ? "https://" : "http://"), oidcContainer.getHost(), - oidcContainer.getPort(), oidcContainer.keycloakX); + String internalBaseUrl = getBaseURL((oidcContainer.isHttps() ? "https://" : "http://"), oidcContainer.getHost(), + oidcContainer.getPort()); + String internalUrl = startURL(internalBaseUrl, oidcContainer.keycloakX); String hostUrl = oidcContainer.useSharedNetwork // we need to use auto-detected host and port, so it works when docker host != localhost ? startURL("http://", oidcContainer.getSharedNetworkExternalHost(), @@ -407,7 +417,7 @@ private static RunningDevService startContainer( : null; Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, internalUrl, hostUrl, - oidcContainer.realmReps, errors, devServicesConfigurator); + oidcContainer.realmReps, errors, devServicesConfigurator, internalBaseUrl); return new RunningDevService(KEYCLOAK_CONTAINER_NAME, oidcContainer.getContainerId(), oidcContainer::close, configs); }; @@ -415,9 +425,9 @@ private static RunningDevService startContainer( return maybeContainerAddress .map(containerAddress -> { // TODO: this probably needs to be addressed - Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, - getSharedContainerUrl(containerAddress), - getSharedContainerUrl(containerAddress), null, errors, devServicesConfigurator); + String sharedContainerUrl = getSharedContainerUrl(containerAddress); + Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, sharedContainerUrl, + sharedContainerUrl, null, errors, devServicesConfigurator, sharedContainerUrl); return new RunningDevService(KEYCLOAK_CONTAINER_NAME, containerAddress.getId(), null, configs); }) .orElseGet(defaultKeycloakContainerSupplier); diff --git a/extensions/keycloak-admin-client-common/runtime/src/main/java/io/quarkus/keycloak/admin/client/common/KeycloakAdminClientConfig.java b/extensions/keycloak-admin-client-common/runtime/src/main/java/io/quarkus/keycloak/admin/client/common/KeycloakAdminClientConfig.java index 004200936b2bd8..aab53d23fda505 100644 --- a/extensions/keycloak-admin-client-common/runtime/src/main/java/io/quarkus/keycloak/admin/client/common/KeycloakAdminClientConfig.java +++ b/extensions/keycloak-admin-client-common/runtime/src/main/java/io/quarkus/keycloak/admin/client/common/KeycloakAdminClientConfig.java @@ -16,9 +16,10 @@ public interface KeycloakAdminClientConfig { /** * Keycloak server URL, for example, `https://host:port`. - * If this property is not set then the Keycloak Admin Client injection will fail - use - * {@linkplain org.keycloak.admin.client.KeycloakBuilder} - * to create it instead. + * When the Keycloak Dev Services is started and this property is not configured, + * Quarkus points the 'quarkus.keycloak.admin-client.server-url' configuration property to started Keycloak container. + * In other cases, when this property is not set then the Keycloak Admin Client injection will fail - use + * {@linkplain org.keycloak.admin.client.KeycloakBuilder} to create the client instead. */ Optional serverUrl(); diff --git a/extensions/keycloak-admin-rest-client/deployment/pom.xml b/extensions/keycloak-admin-rest-client/deployment/pom.xml index 8ff302c1ac65f8..32d5c33b5db811 100644 --- a/extensions/keycloak-admin-rest-client/deployment/pom.xml +++ b/extensions/keycloak-admin-rest-client/deployment/pom.xml @@ -29,6 +29,10 @@ io.quarkus quarkus-keycloak-admin-client-common-deployment + + io.quarkus + quarkus-devservices-keycloak + io.quarkus @@ -45,11 +49,6 @@ quarkus-rest-jackson-deployment test - - io.quarkus - quarkus-oidc-deployment - test - io.smallrye.certs smallrye-certificate-generator-junit5 diff --git a/extensions/keycloak-admin-rest-client/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/devservices/KeycloakDevServiceRequiredBuildStep.java b/extensions/keycloak-admin-rest-client/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/devservices/KeycloakDevServiceRequiredBuildStep.java new file mode 100644 index 00000000000000..1d798dda515939 --- /dev/null +++ b/extensions/keycloak-admin-rest-client/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/devservices/KeycloakDevServiceRequiredBuildStep.java @@ -0,0 +1,32 @@ +package io.quarkus.keycloak.admin.client.reactive.devservices; + +import java.util.Map; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem; +import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class, + KeycloakAdminClientInjectionEnabled.class }) +public class KeycloakDevServiceRequiredBuildStep { + + private static final String SERVER_URL_CONFIG_KEY = "quarkus.keycloak.admin-client.server-url"; + + @BuildStep + KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() { + return KeycloakDevServicesRequiredBuildItem.of( + ctx -> Map.of(SERVER_URL_CONFIG_KEY, ctx.authServerInternalBaseUrl()), + SERVER_URL_CONFIG_KEY); + } + + @BuildStep(onlyIf = IsDevelopment.class) + KeycloakAdminPageBuildItem addCardWithLinkToKeycloakAdmin() { + return new KeycloakAdminPageBuildItem(new CardPageBuildItem()); + } +} diff --git a/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientInjectionDevServicesTest.java b/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientInjectionDevServicesTest.java index b2c20096410c6c..1551f3dfb1577d 100644 --- a/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientInjectionDevServicesTest.java +++ b/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientInjectionDevServicesTest.java @@ -15,17 +15,23 @@ import org.keycloak.admin.client.Keycloak; import org.keycloak.representations.idm.RoleRepresentation; -import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.restassured.response.Response; public class KeycloakAdminClientInjectionDevServicesTest { @RegisterExtension - final static QuarkusDevModeTest app = new QuarkusDevModeTest() + final static QuarkusUnitTest app = new QuarkusUnitTest() .withApplicationRoot(jar -> jar .addClasses(AdminResource.class) - .addAsResource("app-dev-mode-config.properties", "application.properties")); + .addAsResource("app-dev-mode-config.properties", "application.properties")) + // intention of this forced dependency is to test backwards compatibility + // when users started Keycloak Dev Service by adding OIDC extension and configured 'server-url' + .setForcedDependencies( + List.of(Dependency.of("io.quarkus", "quarkus-oidc-deployment", Version.getVersion()))); @Test public void testGetRoles() { diff --git a/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientMutualTlsDevServicesTest.java b/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientMutualTlsDevServicesTest.java index 01542b07702348..753eabfe19bc3c 100644 --- a/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientMutualTlsDevServicesTest.java +++ b/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientMutualTlsDevServicesTest.java @@ -18,7 +18,9 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; -import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.smallrye.certs.Format; import io.smallrye.certs.junit5.Certificate; @@ -29,14 +31,18 @@ public class KeycloakAdminClientMutualTlsDevServicesTest { @RegisterExtension - final static QuarkusDevModeTest app = new QuarkusDevModeTest() + final static QuarkusUnitTest app = new QuarkusUnitTest() .withApplicationRoot(jar -> jar .addClasses(MtlsResource.class) .addAsResource(new File("target/certs/mtls-test-keystore.p12"), "server-keystore.p12") .addAsResource(new File("target/certs/mtls-test-server-ca.crt"), "server-ca.crt") .addAsResource(new File("target/certs/mtls-test-client-keystore.p12"), "client-keystore.p12") .addAsResource(new File("target/certs/mtls-test-client-truststore.p12"), "client-truststore.p12") - .addAsResource("app-mtls-config.properties", "application.properties")); + .addAsResource("app-mtls-config.properties", "application.properties")) + // intention of this forced dependency is to test backwards compatibility + // when users started Keycloak Dev Service by adding OIDC extension and configured 'server-url' + .setForcedDependencies( + List.of(Dependency.of("io.quarkus", "quarkus-oidc-deployment", Version.getVersion()))); @Test public void testCreateRealm() { diff --git a/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientZeroConfigDevServicesTest.java b/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientZeroConfigDevServicesTest.java new file mode 100644 index 00000000000000..fe3853598015a3 --- /dev/null +++ b/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientZeroConfigDevServicesTest.java @@ -0,0 +1,53 @@ +package io.quarkus.keycloak.admin.client.reactive; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.representations.idm.RoleRepresentation; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; +import io.restassured.response.Response; + +public class KeycloakAdminClientZeroConfigDevServicesTest { + + @RegisterExtension + final static QuarkusDevModeTest app = new QuarkusDevModeTest() + .withApplicationRoot(jar -> jar.addClasses(AdminResource.class)); + + @Test + public void testGetRoles() { + // use 'password' grant type + final Response getRolesReq = RestAssured.given().get("/api/admin/roles"); + assertEquals(200, getRolesReq.statusCode()); + final List roles = getRolesReq.jsonPath().getList(".", RoleRepresentation.class); + assertNotNull(roles); + // assert there are roles admin and user (among others) + assertTrue(roles.stream().anyMatch(rr -> "user".equals(rr.getName()))); + assertTrue(roles.stream().anyMatch(rr -> "admin".equals(rr.getName()))); + } + + @Path("/api/admin") + public static class AdminResource { + + @Inject + Keycloak keycloak; + + @GET + @Path("/roles") + public List getRoles() { + return keycloak.realm("quarkus").roles().list(); + } + + } +} diff --git a/extensions/keycloak-admin-rest-client/deployment/src/test/resources/app-dev-mode-config.properties b/extensions/keycloak-admin-rest-client/deployment/src/test/resources/app-dev-mode-config.properties index 47e6f9633d8e6b..ab1a9dac132891 100644 --- a/extensions/keycloak-admin-rest-client/deployment/src/test/resources/app-dev-mode-config.properties +++ b/extensions/keycloak-admin-rest-client/deployment/src/test/resources/app-dev-mode-config.properties @@ -2,5 +2,4 @@ quarkus.keycloak.devservices.port=8082 # Configure Keycloak Admin Client -quarkus.keycloak.admin-client=true quarkus.keycloak.admin-client.server-url=http://localhost:${quarkus.keycloak.devservices.port} diff --git a/extensions/keycloak-admin-resteasy-client/deployment/pom.xml b/extensions/keycloak-admin-resteasy-client/deployment/pom.xml index 882b34d8c82300..598684441f3001 100644 --- a/extensions/keycloak-admin-resteasy-client/deployment/pom.xml +++ b/extensions/keycloak-admin-resteasy-client/deployment/pom.xml @@ -37,6 +37,10 @@ io.quarkus quarkus-keycloak-admin-client-common-deployment + + io.quarkus + quarkus-devservices-keycloak + io.quarkus @@ -53,11 +57,6 @@ quarkus-resteasy-jackson-deployment test - - io.quarkus - quarkus-oidc-deployment - test - io.smallrye.certs smallrye-certificate-generator-junit5 diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/devservices/KeycloakDevServiceRequiredBuildStep.java b/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/devservices/KeycloakDevServiceRequiredBuildStep.java new file mode 100644 index 00000000000000..1d330fa8b9ded2 --- /dev/null +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/devservices/KeycloakDevServiceRequiredBuildStep.java @@ -0,0 +1,32 @@ +package io.quarkus.keycloak.adminclient.deployment.devservices; + +import java.util.Map; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem; +import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class, + KeycloakAdminClientInjectionEnabled.class }) +public class KeycloakDevServiceRequiredBuildStep { + + private static final String SERVER_URL_CONFIG_KEY = "quarkus.keycloak.admin-client.server-url"; + + @BuildStep + KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() { + return KeycloakDevServicesRequiredBuildItem.of( + ctx -> Map.of(SERVER_URL_CONFIG_KEY, ctx.authServerInternalBaseUrl()), + SERVER_URL_CONFIG_KEY); + } + + @BuildStep(onlyIf = IsDevelopment.class) + KeycloakAdminPageBuildItem addCardWithLinkToKeycloakAdmin() { + return new KeycloakAdminPageBuildItem(new CardPageBuildItem()); + } +} diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientInjectionDevServicesTest.java b/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientInjectionDevServicesTest.java index 023213d4b211b7..a87d149b61ca5d 100644 --- a/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientInjectionDevServicesTest.java +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientInjectionDevServicesTest.java @@ -15,17 +15,23 @@ import org.keycloak.admin.client.Keycloak; import org.keycloak.representations.idm.RoleRepresentation; -import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.restassured.response.Response; public class KeycloakAdminClientInjectionDevServicesTest { @RegisterExtension - final static QuarkusDevModeTest app = new QuarkusDevModeTest() + final static QuarkusUnitTest app = new QuarkusUnitTest() .withApplicationRoot(jar -> jar .addClasses(AdminResource.class) - .addAsResource("app-dev-mode-config.properties", "application.properties")); + .addAsResource("app-dev-mode-config.properties", "application.properties")) + // intention of this forced dependency is to test backwards compatibility + // when users started Keycloak Dev Service by adding OIDC extension and configured 'server-url' + .setForcedDependencies( + List.of(Dependency.of("io.quarkus", "quarkus-oidc-deployment", Version.getVersion()))); @Test public void testGetRoles() { diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientMutualTlsDevServicesTest.java b/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientMutualTlsDevServicesTest.java index b628adb65aed5f..33eaac20f98ad3 100644 --- a/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientMutualTlsDevServicesTest.java +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientMutualTlsDevServicesTest.java @@ -18,7 +18,9 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; -import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.smallrye.certs.Format; import io.smallrye.certs.junit5.Certificate; @@ -29,14 +31,18 @@ public class KeycloakAdminClientMutualTlsDevServicesTest { @RegisterExtension - final static QuarkusDevModeTest app = new QuarkusDevModeTest() + final static QuarkusUnitTest app = new QuarkusUnitTest() .withApplicationRoot(jar -> jar .addClasses(MtlsResource.class) .addAsResource(new File("target/certs/mtls-test-keystore.p12"), "server-keystore.p12") .addAsResource(new File("target/certs/mtls-test-server-ca.crt"), "server-ca.crt") .addAsResource(new File("target/certs/mtls-test-client-keystore.p12"), "client-keystore.p12") .addAsResource(new File("target/certs/mtls-test-client-truststore.p12"), "client-truststore.p12") - .addAsResource("app-mtls-config.properties", "application.properties")); + .addAsResource("app-mtls-config.properties", "application.properties")) + // intention of this forced dependency is to test backwards compatibility + // when users started Keycloak Dev Service by adding OIDC extension and configured 'server-url' + .setForcedDependencies( + List.of(Dependency.of("io.quarkus", "quarkus-oidc-deployment", Version.getVersion()))); @Test public void testCreateRealm() { diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientZeroConfigDevServicesTest.java b/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientZeroConfigDevServicesTest.java new file mode 100644 index 00000000000000..440ed7514621da --- /dev/null +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientZeroConfigDevServicesTest.java @@ -0,0 +1,53 @@ +package io.quarkus.keycloak.adminclient.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.representations.idm.RoleRepresentation; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; +import io.restassured.response.Response; + +public class KeycloakAdminClientZeroConfigDevServicesTest { + + @RegisterExtension + final static QuarkusDevModeTest app = new QuarkusDevModeTest() + .withApplicationRoot(jar -> jar.addClasses(AdminResource.class)); + + @Test + public void testGetRoles() { + // use 'password' grant type + final Response getRolesReq = RestAssured.given().get("/api/admin/roles"); + assertEquals(200, getRolesReq.statusCode()); + final List roles = getRolesReq.jsonPath().getList(".", RoleRepresentation.class); + assertNotNull(roles); + // assert there are roles admin and user (among others) + assertTrue(roles.stream().anyMatch(rr -> "user".equals(rr.getName()))); + assertTrue(roles.stream().anyMatch(rr -> "admin".equals(rr.getName()))); + } + + @Path("/api/admin") + public static class AdminResource { + + @Inject + Keycloak keycloak; + + @GET + @Path("/roles") + public List getRoles() { + return keycloak.realm("quarkus").roles().list(); + } + + } +} diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/test/resources/app-dev-mode-config.properties b/extensions/keycloak-admin-resteasy-client/deployment/src/test/resources/app-dev-mode-config.properties index 47e6f9633d8e6b..ab1a9dac132891 100644 --- a/extensions/keycloak-admin-resteasy-client/deployment/src/test/resources/app-dev-mode-config.properties +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/test/resources/app-dev-mode-config.properties @@ -2,5 +2,4 @@ quarkus.keycloak.devservices.port=8082 # Configure Keycloak Admin Client -quarkus.keycloak.admin-client=true quarkus.keycloak.admin-client.server-url=http://localhost:${quarkus.keycloak.devservices.port} diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/DefaultPolicyEnforcerResolver.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/DefaultPolicyEnforcerResolver.java index f7ec6d61f72156..92c560fc6946c1 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/DefaultPolicyEnforcerResolver.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/DefaultPolicyEnforcerResolver.java @@ -49,7 +49,7 @@ public class DefaultPolicyEnforcerResolver implements PolicyEnforcerResolver { this.tlsSupport = OidcTlsSupport.empty(); } - var defaultTenantConfig = new OidcTenantConfig(oidcConfig.defaultTenant(), OidcUtils.DEFAULT_TENANT_ID); + var defaultTenantConfig = new OidcTenantConfig(OidcConfig.getDefaultTenant(oidcConfig), OidcUtils.DEFAULT_TENANT_ID); var defaultTenantTlsSupport = tlsSupport.forConfig(defaultTenantConfig.tls); this.defaultPolicyEnforcer = createPolicyEnforcer(defaultTenantConfig, config.defaultTenant(), defaultTenantTlsSupport); diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerUtil.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerUtil.java index 47330c2f01caa8..4ee31b741933fc 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerUtil.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerUtil.java @@ -226,7 +226,7 @@ private static boolean isNotComplexConfigKey(String key) { static OidcTenantConfig getOidcTenantConfig(OidcConfig oidcConfig, String tenant) { if (tenant == null || DEFAULT_TENANT_ID.equals(tenant)) { - return new OidcTenantConfig(oidcConfig.defaultTenant(), DEFAULT_TENANT_ID); + return new OidcTenantConfig(OidcConfig.getDefaultTenant(oidcConfig), DEFAULT_TENANT_ID); } var oidcTenantConfig = oidcConfig.namedTenants().get(tenant); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNodeSelectorDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNodeSelectorDecorator.java new file mode 100644 index 00000000000000..c646ac9525ef92 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNodeSelectorDecorator.java @@ -0,0 +1,24 @@ +package io.quarkus.kubernetes.deployment; + +import io.dekorate.kubernetes.decorator.NamedResourceDecorator; +import io.dekorate.utils.Strings; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.PodSpecFluent; + +public class AddNodeSelectorDecorator extends NamedResourceDecorator> { + private final String nodeSelectorKey; + private final String nodeSelectorValue; + + public AddNodeSelectorDecorator(String deploymentName, String nodeSelectorKey, String nodeSelectorValue) { + super(deploymentName); + this.nodeSelectorKey = nodeSelectorKey; + this.nodeSelectorValue = nodeSelectorValue; + } + + public void andThenVisit(PodSpecFluent podSpec, ObjectMeta resourceMeta) { + if (Strings.isNotNullOrEmpty(nodeSelectorKey) && Strings.isNotNullOrEmpty(nodeSelectorValue)) { + podSpec.removeFromNodeSelector(nodeSelectorKey); + podSpec.addToNodeSelector(nodeSelectorKey, nodeSelectorValue); + } + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java index 03aa9446595b1f..a19726c00c5da0 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java @@ -290,6 +290,8 @@ public List createDecorators(ApplicationInfoBuildItem applic result.addAll(createAppConfigVolumeAndEnvDecorators(name, config)); config.hostAliases().entrySet().forEach(e -> result.add(new DecoratorBuildItem(KNATIVE, new AddHostAliasesToRevisionDecorator(name, HostAliasConverter.convert(e))))); + config.nodeSelector().ifPresent(n -> result.add(new DecoratorBuildItem(KNATIVE, + new AddNodeSelectorDecorator(name, n.key(), n.value())))); config.sidecars().entrySet().forEach(e -> result .add(new DecoratorBuildItem(KNATIVE, new AddSidecarToRevisionDecorator(name, ContainerConverter.convert(e))))); 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 68160092bc238e..6c1f43f2e3d210 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 @@ -852,6 +852,10 @@ private static List createPodDecorators(String target, Strin config.hostAliases().entrySet().forEach(e -> result .add(new DecoratorBuildItem(target, new AddHostAliasesDecorator(name, HostAliasConverter.convert(e))))); + config.nodeSelector() + .ifPresent(n -> result.add( + new DecoratorBuildItem(target, new AddNodeSelectorDecorator(name, n.key(), n.value())))); + config.initContainers().entrySet().forEach(e -> result .add(new DecoratorBuildItem(target, new AddInitContainerDecorator(name, ContainerConverter.convert(e))))); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/NodeSelectorConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/NodeSelectorConfig.java new file mode 100644 index 00000000000000..681e7de03043ea --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/NodeSelectorConfig.java @@ -0,0 +1,13 @@ +package io.quarkus.kubernetes.deployment; + +public interface NodeSelectorConfig { + /** + * The key of the nodeSelector. + */ + String key(); + + /** + * The value of the nodeSelector. + */ + String value(); +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java index 11c78c45cc24fe..51a8e40ab7fc33 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java @@ -204,6 +204,11 @@ public interface PlatformConfiguration extends EnvVarHolder { @WithName("hostaliases") Map hostAliases(); + /** + * The nodeSelector. + */ + Optional nodeSelector(); + /** * Resources requirements. */ diff --git a/extensions/kubernetes/vanilla/deployment/src/test/java/io/quarkus/kubernetes/deployment/KubernetesConfigTest.java b/extensions/kubernetes/vanilla/deployment/src/test/java/io/quarkus/kubernetes/deployment/KubernetesConfigTest.java index 4e75fe48d3e1fd..1ddc0935099872 100644 --- a/extensions/kubernetes/vanilla/deployment/src/test/java/io/quarkus/kubernetes/deployment/KubernetesConfigTest.java +++ b/extensions/kubernetes/vanilla/deployment/src/test/java/io/quarkus/kubernetes/deployment/KubernetesConfigTest.java @@ -228,6 +228,10 @@ void kubernetes() throws Exception { assertEquals("konoha", hostAliases.get("ip")); assertIterableEquals(List.of("dev", "qly", "prod"), (Iterable) hostAliases.get("hostnames")); + Map nodeSelector = deployment().map("spec").map("template").map("spec").asMap("nodeSelector"); + assertTrue(nodeSelector.containsKey("jutsu")); + assertEquals("katon", nodeSelector.get("jutsu")); + Map limits = container().map("resources").asMap("limits"); assertEquals("fuuton", limits.get("cpu")); assertEquals("raiton", limits.get("memory")); diff --git a/extensions/kubernetes/vanilla/deployment/src/test/resources/application-kubernetes.properties b/extensions/kubernetes/vanilla/deployment/src/test/resources/application-kubernetes.properties index 467f9ad9fcb824..bdf4d8a9145444 100644 --- a/extensions/kubernetes/vanilla/deployment/src/test/resources/application-kubernetes.properties +++ b/extensions/kubernetes/vanilla/deployment/src/test/resources/application-kubernetes.properties @@ -132,6 +132,9 @@ quarkus.kubernetes.deployment-target=kubernetes,openshift,knative,minikube quarkus.kubernetes.hostaliases.konoha.ip=0.0.0.0 quarkus.kubernetes.hostaliases.konoha.hostnames=dev,qly,prod +quarkus.kubernetes.node-selector.key=jutsu +quarkus.kubernetes.node-selector.value=katon + quarkus.kubernetes.resources.limits.cpu=fuuton quarkus.kubernetes.resources.limits.memory=raiton quarkus.kubernetes.resources.requests.cpu=katon diff --git a/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java index 47ef0ba81a3327..44db47e3c9b7f8 100644 --- a/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java +++ b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java @@ -5,6 +5,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; import io.quarkus.deployment.builditem.LogFileFormatBuildItem; +import io.quarkus.deployment.builditem.LogSocketFormatBuildItem; import io.quarkus.deployment.builditem.LogSyslogFormatBuildItem; import io.quarkus.logging.json.runtime.JsonLogConfig; import io.quarkus.logging.json.runtime.LoggingJsonRecorder; @@ -28,4 +29,10 @@ public LogFileFormatBuildItem setUpFileFormatter(LoggingJsonRecorder recorder, J public LogSyslogFormatBuildItem setUpSyslogFormatter(LoggingJsonRecorder recorder, JsonLogConfig config) { return new LogSyslogFormatBuildItem(recorder.initializeSyslogJsonLogging(config)); } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public LogSocketFormatBuildItem setUpSocketFormatter(LoggingJsonRecorder recorder, JsonLogConfig config) { + return new LogSocketFormatBuildItem(recorder.initializeSocketJsonLogging(config)); + } } diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SocketJsonFormatterCustomConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SocketJsonFormatterCustomConfigTest.java new file mode 100644 index 00000000000000..e6e0d8a9ac0a26 --- /dev/null +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SocketJsonFormatterCustomConfigTest.java @@ -0,0 +1,72 @@ +package io.quarkus.logging.json; + +import static io.quarkus.logging.json.SocketJsonFormatterDefaultConfigTest.getJsonFormatter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZoneId; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.assertj.core.api.Assertions; +import org.jboss.logmanager.formatters.StructuredFormatter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.logging.json.runtime.AdditionalFieldConfig; +import io.quarkus.logging.json.runtime.JsonFormatter; +import io.quarkus.test.QuarkusUnitTest; + +public class SocketJsonFormatterCustomConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(SocketJsonFormatterDefaultConfigTest.class)) + .withConfigurationResource("application-socket-json-formatter-custom.properties"); + + @Test + public void jsonFormatterCustomConfigurationTest() { + JsonFormatter jsonFormatter = getJsonFormatter(); + assertThat(jsonFormatter.isPrettyPrint()).isTrue(); + assertThat(jsonFormatter.getDateTimeFormatter().toString()) + .isEqualTo("Value(DayOfMonth)' 'Text(MonthOfYear,SHORT)' 'Value(Year,4,19,EXCEEDS_PAD)"); + assertThat(jsonFormatter.getDateTimeFormatter().getZone()).isEqualTo(ZoneId.of("UTC+05:00")); + assertThat(jsonFormatter.getExceptionOutputType()) + .isEqualTo(StructuredFormatter.ExceptionOutputType.DETAILED_AND_FORMATTED); + assertThat(jsonFormatter.getRecordDelimiter()).isEqualTo("\n;"); + assertThat(jsonFormatter.isPrintDetails()).isTrue(); + assertThat(jsonFormatter.getExcludedKeys()).containsExactly("timestamp", "sequence"); + assertThat(jsonFormatter.getAdditionalFields().size()).isEqualTo(2); + assertThat(jsonFormatter.getAdditionalFields().containsKey("foo")).isTrue(); + assertThat(jsonFormatter.getAdditionalFields().get("foo").type).isEqualTo(AdditionalFieldConfig.Type.INT); + assertThat(jsonFormatter.getAdditionalFields().get("foo").value).isEqualTo("42"); + assertThat(jsonFormatter.getAdditionalFields().containsKey("bar")).isTrue(); + assertThat(jsonFormatter.getAdditionalFields().get("bar").type).isEqualTo(AdditionalFieldConfig.Type.STRING); + assertThat(jsonFormatter.getAdditionalFields().get("bar").value).isEqualTo("baz"); + } + + @Test + public void jsonFormatterOutputTest() throws Exception { + JsonFormatter jsonFormatter = getJsonFormatter(); + String line = jsonFormatter.format(new LogRecord(Level.INFO, "Hello, World!")); + + JsonNode node = new ObjectMapper().readTree(line); + // "level" has been renamed to HEY + Assertions.assertThat(node.has("level")).isFalse(); + Assertions.assertThat(node.has("HEY")).isTrue(); + Assertions.assertThat(node.get("HEY").asText()).isEqualTo("INFO"); + + // excluded fields + Assertions.assertThat(node.has("timestamp")).isFalse(); + Assertions.assertThat(node.has("sequence")).isFalse(); + + // additional fields + Assertions.assertThat(node.has("foo")).isTrue(); + Assertions.assertThat(node.get("foo").asInt()).isEqualTo(42); + Assertions.assertThat(node.has("bar")).isTrue(); + Assertions.assertThat(node.get("bar").asText()).isEqualTo("baz"); + Assertions.assertThat(node.get("message").asText()).isEqualTo("Hello, World!"); + } +} diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SocketJsonFormatterDefaultConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SocketJsonFormatterDefaultConfigTest.java new file mode 100644 index 00000000000000..99d25bfa99805f --- /dev/null +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SocketJsonFormatterDefaultConfigTest.java @@ -0,0 +1,62 @@ +package io.quarkus.logging.json; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import org.jboss.logmanager.formatters.StructuredFormatter; +import org.jboss.logmanager.handlers.SocketHandler; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.logging.InitialConfigurator; +import io.quarkus.bootstrap.logging.QuarkusDelayedHandler; +import io.quarkus.logging.json.runtime.JsonFormatter; +import io.quarkus.test.QuarkusUnitTest; + +public class SocketJsonFormatterDefaultConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-socket-json-formatter-default.properties"); + + @Test + public void jsonFormatterDefaultConfigurationTest() { + JsonFormatter jsonFormatter = getJsonFormatter(); + assertThat(jsonFormatter.isPrettyPrint()).isFalse(); + assertThat(jsonFormatter.getDateTimeFormatter().toString()) + .isEqualTo(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()).toString()); + assertThat(jsonFormatter.getDateTimeFormatter().getZone()).isEqualTo(ZoneId.systemDefault()); + assertThat(jsonFormatter.getExceptionOutputType()).isEqualTo(StructuredFormatter.ExceptionOutputType.DETAILED); + assertThat(jsonFormatter.getRecordDelimiter()).isEqualTo("\n"); + assertThat(jsonFormatter.isPrintDetails()).isFalse(); + assertThat(jsonFormatter.getExcludedKeys()).isEmpty(); + assertThat(jsonFormatter.getAdditionalFields().entrySet()).isEmpty(); + } + + public static JsonFormatter getJsonFormatter() { + LogManager logManager = LogManager.getLogManager(); + assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); + + QuarkusDelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; + assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); + assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); + + Handler handler = Arrays.stream(delayedHandler.getHandlers()) + .filter(h -> (h instanceof SocketHandler)) + .findFirst().orElse(null); + assertThat(handler).isNotNull(); + assertThat(handler.getLevel()).isEqualTo(Level.WARNING); + + Formatter formatter = handler.getFormatter(); + assertThat(formatter).isInstanceOf(JsonFormatter.class); + return (JsonFormatter) formatter; + } +} diff --git a/extensions/logging-json/deployment/src/test/resources/application-socket-json-formatter-custom.properties b/extensions/logging-json/deployment/src/test/resources/application-socket-json-formatter-custom.properties new file mode 100644 index 00000000000000..0441faac791597 --- /dev/null +++ b/extensions/logging-json/deployment/src/test/resources/application-socket-json-formatter-custom.properties @@ -0,0 +1,16 @@ +quarkus.log.level=INFO +quarkus.log.socket.enable=true +quarkus.log.socket.level=WARNING +quarkus.log.socket.json=true +quarkus.log.socket.json.pretty-print=true +quarkus.log.socket.json.date-format=d MMM uuuu +quarkus.log.socket.json.record-delimiter=\n; +quarkus.log.socket.json.zone-id=UTC+05:00 +quarkus.log.socket.json.exception-output-type=DETAILED_AND_FORMATTED +quarkus.log.socket.json.print-details=true +quarkus.log.socket.json.key-overrides=level=HEY +quarkus.log.socket.json.excluded-keys=timestamp,sequence +quarkus.log.socket.json.additional-field.foo.value=42 +quarkus.log.socket.json.additional-field.foo.type=int +quarkus.log.socket.json.additional-field.bar.value=baz +quarkus.log.socket.json.additional-field.bar.type=string diff --git a/extensions/logging-json/deployment/src/test/resources/application-socket-json-formatter-default.properties b/extensions/logging-json/deployment/src/test/resources/application-socket-json-formatter-default.properties new file mode 100644 index 00000000000000..31933e9601d377 --- /dev/null +++ b/extensions/logging-json/deployment/src/test/resources/application-socket-json-formatter-default.properties @@ -0,0 +1,5 @@ +quarkus.log.level=INFO +quarkus.log.socket.enable=true +quarkus.log.socket.level=WARNING +quarkus.log.socket.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.socket.json=true diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java index 35ec19204e4419..3f1cd1fffd9d45 100644 --- a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java @@ -40,6 +40,13 @@ public class JsonLogConfig { @ConfigItem(name = "syslog.json") JsonConfig syslogJson; + /** + * Socket logging. + */ + @ConfigDocSection + @ConfigItem(name = "socket.json") + JsonConfig socketJson; + @ConfigGroup public static class JsonConfig { /** @@ -98,5 +105,16 @@ public static class JsonConfig { @ConfigItem @ConfigDocMapKey("field-name") Map additionalField; + + /** + * Specify the format of the produced JSON + */ + @ConfigItem(defaultValue = "DEFAULT") + LogFormat logFormat; + + public enum LogFormat { + DEFAULT, + ECS + } } } diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java index 872f292569dadb..d05745e2a76ca2 100644 --- a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java @@ -1,8 +1,17 @@ package io.quarkus.logging.json.runtime; +import java.util.EnumMap; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import java.util.logging.Formatter; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logmanager.PropertyValues; +import org.jboss.logmanager.formatters.StructuredFormatter.Key; + +import io.quarkus.logging.json.runtime.AdditionalFieldConfig.Type; import io.quarkus.logging.json.runtime.JsonLogConfig.JsonConfig; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; @@ -22,7 +31,19 @@ public RuntimeValue> initializeSyslogJsonLogging(JsonLogConf return getFormatter(config.syslogJson); } + public RuntimeValue> initializeSocketJsonLogging(JsonLogConfig config) { + return getFormatter(config.socketJson); + } + private RuntimeValue> getFormatter(JsonConfig config) { + if (config.logFormat == JsonConfig.LogFormat.ECS) { + addEcsFieldOverrides(config); + } + + return getDefaultFormatter(config); + } + + private RuntimeValue> getDefaultFormatter(JsonConfig config) { if (!config.enable) { return new RuntimeValue<>(Optional.empty()); } @@ -43,4 +64,43 @@ private RuntimeValue> getFormatter(JsonConfig config) { } return new RuntimeValue<>(Optional.of(formatter)); } + + private void addEcsFieldOverrides(JsonConfig config) { + EnumMap keyOverrides = PropertyValues.stringToEnumMap(Key.class, config.keyOverrides.orElse(null)); + keyOverrides.putIfAbsent(Key.TIMESTAMP, "@timestamp"); + keyOverrides.putIfAbsent(Key.LOGGER_NAME, "log.logger"); + keyOverrides.putIfAbsent(Key.LEVEL, "log.level"); + keyOverrides.putIfAbsent(Key.PROCESS_ID, "process.pid"); + keyOverrides.putIfAbsent(Key.PROCESS_NAME, "process.name"); + keyOverrides.putIfAbsent(Key.THREAD_NAME, "process.thread.name"); + keyOverrides.putIfAbsent(Key.THREAD_ID, "process.thread.id"); + keyOverrides.putIfAbsent(Key.HOST_NAME, "host.hostname"); + keyOverrides.putIfAbsent(Key.SEQUENCE, "event.sequence"); + keyOverrides.putIfAbsent(Key.EXCEPTION_MESSAGE, "error.message"); + keyOverrides.putIfAbsent(Key.STACK_TRACE, "error.stack_trace"); + config.keyOverrides = Optional.of(PropertyValues.mapToString(keyOverrides)); + + config.additionalField.computeIfAbsent("ecs.version", k -> buildFieldConfig("1.12.2", Type.STRING)); + config.additionalField.computeIfAbsent("data_stream.type", k -> buildFieldConfig("logs", Type.STRING)); + + Config quarkusConfig = ConfigProvider.getConfig(); + quarkusConfig.getOptionalValue("quarkus.application.name", String.class).ifPresent( + s -> config.additionalField.computeIfAbsent("service.name", k -> buildFieldConfig(s, Type.STRING))); + quarkusConfig.getOptionalValue("quarkus.application.version", String.class).ifPresent( + s -> config.additionalField.computeIfAbsent("service.version", k -> buildFieldConfig(s, Type.STRING))); + quarkusConfig.getOptionalValue("quarkus.profile", String.class).ifPresent( + s -> config.additionalField.computeIfAbsent("service.environment", k -> buildFieldConfig(s, Type.STRING))); + + Set excludedKeys = config.excludedKeys.orElseGet(HashSet::new); + excludedKeys.add(Key.LOGGER_CLASS_NAME.getKey()); + excludedKeys.add(Key.RECORD.getKey()); + config.excludedKeys = Optional.of(excludedKeys); + } + + private AdditionalFieldConfig buildFieldConfig(String value, Type type) { + AdditionalFieldConfig field = new AdditionalFieldConfig(); + field.type = type; + field.value = value; + return field; + } } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java index edba7b764aa0fd..5f55cb5b6f8f42 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java @@ -72,47 +72,47 @@ public final class TransactionManagerConfiguration { */ @ConfigItem public ObjectStoreConfig objectStore; -} - -@ConfigGroup -class ObjectStoreConfig { - /** - * The name of the directory where the transaction logs will be stored when using the {@code file-system} object store. - * If the value is not absolute then the directory is relative - * to the user.dir system property. - */ - @ConfigItem(defaultValue = "ObjectStore") - public String directory; - - /** - * The type of object store. - */ - @ConfigItem(defaultValue = "file-system") - public ObjectStoreType type; - - /** - * The name of the datasource where the transaction logs will be stored when using the {@code jdbc} object store. - *

- * If undefined, it will use the default datasource. - */ - @ConfigItem - public Optional datasource = Optional.empty(); - /** - * Whether to create the table if it does not exist. - */ - @ConfigItem(defaultValue = "false") - public boolean createTable; - - /** - * Whether to drop the table on startup. - */ - @ConfigItem(defaultValue = "false") - public boolean dropTable; - - /** - * The prefix to apply to the table. - */ - @ConfigItem(defaultValue = "quarkus_") - public String tablePrefix; + @ConfigGroup + public static class ObjectStoreConfig { + /** + * The name of the directory where the transaction logs will be stored when using the {@code file-system} object store. + * If the value is not absolute then the directory is relative + * to the user.dir system property. + */ + @ConfigItem(defaultValue = "ObjectStore") + public String directory; + + /** + * The type of object store. + */ + @ConfigItem(defaultValue = "file-system") + public ObjectStoreType type; + + /** + * The name of the datasource where the transaction logs will be stored when using the {@code jdbc} object store. + *

+ * If undefined, it will use the default datasource. + */ + @ConfigItem + public Optional datasource = Optional.empty(); + + /** + * Whether to create the table if it does not exist. + */ + @ConfigItem(defaultValue = "false") + public boolean createTable; + + /** + * Whether to drop the table on startup. + */ + @ConfigItem(defaultValue = "false") + public boolean dropTable; + + /** + * The prefix to apply to the table. + */ + @ConfigItem(defaultValue = "quarkus_") + public String tablePrefix; + } } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java index 8ad80d388a9276..07318e9f4c18ab 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java @@ -4,8 +4,11 @@ import java.util.Optional; import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigGroup; import io.smallrye.config.WithDefault; +@ConfigGroup public interface OidcClientCommonConfig extends OidcCommonConfig { /** * The OIDC token endpoint that issues access and refresh tokens; @@ -34,10 +37,14 @@ public interface OidcClientCommonConfig extends OidcCommonConfig { Optional clientName(); /** - * Credentials the OIDC adapter uses to authenticate to the OIDC server. + * Different authentication options for OIDC client to access OIDC token and other secured endpoints. */ + @ConfigDocSection Credentials credentials(); + /** + * Credentials used by OIDC client to authenticate to OIDC token and other secured endpoints. + */ interface Credentials { /** diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java index 4edf4be8a9d466..1af74365734fda 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java @@ -6,8 +6,11 @@ import java.util.OptionalInt; import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigGroup; import io.smallrye.config.WithDefault; +@ConfigGroup public interface OidcCommonConfig { /** * The base URL of the OpenID Connect (OIDC) server, for example, `https://host:port/auth`. @@ -75,13 +78,15 @@ public interface OidcCommonConfig { boolean followRedirects(); /** - * Options to configure the proxy the OIDC adapter uses to talk with the OIDC server. + * HTTP proxy configuration. */ + @ConfigDocSection Proxy proxy(); /** - * TLS configurations + * TLS configuration. */ + @ConfigDocSection Tls tls(); interface Tls { diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index ee5af7e5b9afc6..f47a1df8d44324 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -230,28 +230,12 @@ public boolean appliesTo(Type requiredType) { @Override public void transform(TransformationContext ctx) { - if (ctx.getTarget().kind() == METHOD) { + var tenantAnnotation = Annotations.find(ctx.getAllTargetAnnotations(), TENANT_NAME); + if (tenantAnnotation != null && tenantAnnotation.value() != null) { ctx - .getAllAnnotations() - .stream() - .filter(a -> TENANT_NAME.equals(a.name())) - .forEach(a -> { - var annotationValue = new AnnotationValue[] { - AnnotationValue.createStringValue("value", a.value().asString()) }; - ctx - .transform() - .add(AnnotationInstance.create(NAMED, a.target(), annotationValue)) - .done(); - }); - } else { - // field - var tenantAnnotation = Annotations.find(ctx.getAllAnnotations(), TENANT_NAME); - if (tenantAnnotation != null && tenantAnnotation.value() != null) { - ctx - .transform() - .add(NAMED, AnnotationValue.createStringValue("value", tenantAnnotation.value().asString())) - .done(); - } + .transform() + .add(NAMED, AnnotationValue.createStringValue("value", tenantAnnotation.value().asString())) + .done(); } } }); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java index 58cc8d51e3722c..2060330618e6ce 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java @@ -1,6 +1,7 @@ package io.quarkus.oidc.deployment; import io.quarkus.oidc.runtime.OidcConfig; +import io.quarkus.runtime.annotations.ConfigDocSection; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; @@ -18,8 +19,9 @@ public interface OidcBuildTimeConfig { boolean enabled(); /** - * Dev UI configuration. + * OIDC Dev UI configuration which is effective in dev mode only. */ + @ConfigDocSection DevUiConfig devui(); /** diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java index b4dd761333862c..c3fd1eb15538b6 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java @@ -45,7 +45,7 @@ public void logout() { @Path("access-token-name") @GET public String accessTokenName() { - if (!config.defaultTenant().authentication().verifyAccessToken()) { + if (!OidcConfig.getDefaultTenant(config).authentication().verifyAccessToken()) { throw new IllegalStateException("Access token verification should be enabled"); } return accessToken.getName(); diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithJwtAccessToken.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithJwtAccessToken.java index 59d17dd128ab7d..a1adaa128fcce7 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithJwtAccessToken.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithJwtAccessToken.java @@ -26,6 +26,6 @@ public class ProtectedResourceWithJwtAccessToken { @GET public String getName() { - return idToken.getName() + ":" + config.defaultTenant().authentication().verifyAccessToken(); + return idToken.getName() + ":" + OidcConfig.getDefaultTenant(config).authentication().verifyAccessToken(); } } diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithoutJwtAccessToken.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithoutJwtAccessToken.java index 3305810d605be8..8e624842499a7f 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithoutJwtAccessToken.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithoutJwtAccessToken.java @@ -23,6 +23,6 @@ public class ProtectedResourceWithoutJwtAccessToken { @GET public String getName() { - return idToken.getName() + ":" + config.defaultTenant().authentication().verifyAccessToken(); + return idToken.getName() + ":" + OidcConfig.getDefaultTenant(config).authentication().verifyAccessToken(); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java index 020a3ba2dc80ae..e1679ef7b851aa 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java @@ -37,7 +37,7 @@ public BackChannelLogoutHandler(OidcConfig oidcConfig) { } public void setup(@Observes Router router) { - addRoute(router, new OidcTenantConfig(oidcConfig.defaultTenant(), OidcUtils.DEFAULT_TENANT_ID)); + addRoute(router, new OidcTenantConfig(OidcConfig.getDefaultTenant(oidcConfig), OidcUtils.DEFAULT_TENANT_ID)); for (var nameToOidcTenantConfig : oidcConfig.namedTenants().entrySet()) { addRoute(router, new OidcTenantConfig(nameToOidcTenantConfig.getValue(), nameToOidcTenantConfig.getKey())); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java index a92a8ff0f67bf3..c0632cb11c887f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java @@ -10,31 +10,31 @@ import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; +import io.smallrye.config.WithDefaults; import io.smallrye.config.WithParentName; +import io.smallrye.config.WithUnnamedKey; @ConfigMapping(prefix = "quarkus.oidc") @ConfigRoot(phase = ConfigPhase.RUN_TIME) public interface OidcConfig { - /** - * The default tenant. - */ - @WithParentName - OidcTenantConfig defaultTenant(); + String DEFAULT_TENANT_KEY = ""; /** * Additional named tenants. */ - @ConfigDocSection @ConfigDocMapKey("tenant") @WithParentName + @WithUnnamedKey(DEFAULT_TENANT_KEY) + @WithDefaults Map namedTenants(); /** - * Default TokenIntrospection and UserInfo Cache configuration which is used for all the tenants if it is enabled - * with the build-time 'quarkus.oidc.default-token-cache-enabled' property ('true' by default) and also activated, - * see its `max-size` property. + * Default TokenIntrospection and UserInfo Cache configuration. + * It is used for all the tenants if it is enabled with the build-time 'quarkus.oidc.default-token-cache-enabled' property + * ('true' by default) and also activated, see its `max-size` property. */ + @ConfigDocSection TokenCache tokenCache(); /** @@ -66,4 +66,13 @@ interface TokenCache { */ Optional cleanUpTimerInterval(); } + + static io.quarkus.oidc.runtime.OidcTenantConfig getDefaultTenant(OidcConfig config) { + for (var tenant : config.namedTenants().entrySet()) { + if (OidcConfig.DEFAULT_TENANT_KEY.equals(tenant.getKey())) { + return tenant.getValue(); + } + } + return null; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index 9dd8de738e6ee4..086148a1eb96f3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -111,7 +111,7 @@ public TenantConfigBean setup(OidcConfig config, Vertx vertxValue, OidcTlsSuppor boolean userInfoInjectionPointDetected) { OidcRecorder.userInfoInjectionPointDetected = userInfoInjectionPointDetected; - var defaultTenant = new OidcTenantConfig(config.defaultTenant(), DEFAULT_TENANT_ID); + var defaultTenant = new OidcTenantConfig(OidcConfig.getDefaultTenant(config), DEFAULT_TENANT_ID); String defaultTenantId = defaultTenant.getTenantId().get(); var defaultTenantInitializer = createStaticTenantContextCreator(vertxValue, defaultTenant, !config.namedTenants().isEmpty(), defaultTenantId, tlsSupport); @@ -120,6 +120,9 @@ public TenantConfigBean setup(OidcConfig config, Vertx vertxValue, OidcTlsSuppor Map staticTenantsConfig = new HashMap<>(); for (var tenant : config.namedTenants().entrySet()) { + if (OidcConfig.DEFAULT_TENANT_KEY.equals(tenant.getKey())) { + continue; + } var namedTenantConfig = new OidcTenantConfig(tenant.getValue(), tenant.getKey()); OidcCommonUtils.verifyConfigurationId(defaultTenantId, tenant.getKey(), namedTenantConfig.getTenantId()); var staticTenantInitializer = createStaticTenantContextCreator(vertxValue, namedTenantConfig, false, @@ -709,7 +712,7 @@ private TenantSpecificOidcIdentityProvider(String tenantId) { this.blockingExecutor = Arc.container().instance(BlockingSecurityExecutor.class).get(); if (tenantId.equals(DEFAULT_TENANT_ID)) { OidcConfig config = Arc.container().instance(OidcConfig.class).get(); - this.tenantId = config.defaultTenant().tenantId().orElse(OidcUtils.DEFAULT_TENANT_ID); + this.tenantId = OidcConfig.getDefaultTenant(config).tenantId().orElse(OidcUtils.DEFAULT_TENANT_ID); } else { this.tenantId = tenantId; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java index bd340066396142..f3c9c1463458c3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java @@ -16,6 +16,7 @@ import io.quarkus.oidc.common.runtime.config.OidcCommonConfig; import io.quarkus.runtime.annotations.ConfigDocDefault; import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; import io.quarkus.runtime.configuration.TrimmedStringConverter; import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.smallrye.config.WithConverter; @@ -103,14 +104,16 @@ public interface OidcTenantConfig extends OidcClientCommonConfig { Optional publicKey(); /** - * Introspection Basic Authentication which must be configured only if the introspection is required - * and OpenId Connect Provider does not support the OIDC client authentication configured with + * Optional introspection endpoint-specific basic authentication configuration. + * It must be configured only if the introspection is required + * but OpenId Connect Provider does not support the OIDC client authentication configured with * {@link OidcCommonConfig#credentials} for its introspection endpoint. */ + @ConfigDocSection IntrospectionCredentials introspectionCredentials(); /** - * Introspection Basic Authentication configuration + * Optional introspection endpoint-specific authentication configuration. */ interface IntrospectionCredentials { /** @@ -132,18 +135,21 @@ interface IntrospectionCredentials { } /** - * Configuration to find and parse a custom claim containing the roles information. + * Configuration to find and parse custom claims which contain roles. */ + @ConfigDocSection Roles roles(); /** - * Configuration how to validate the token claims. + * Configuration to customize validation of token claims. */ + @ConfigDocSection Token token(); /** - * RP Initiated, BackChannel and FrontChannel Logout configuration + * RP-initiated, back-channel and front-channel logout configuration. */ + @ConfigDocSection Logout logout(); /** @@ -161,8 +167,12 @@ interface IntrospectionCredentials { * If the truststore does not have the leaf certificate imported, then the leaf certificate must be identified by its Common * Name. */ + @ConfigDocSection CertificateChain certificateChain(); + /** + * Configuration of the certificate chain which can be used to verify tokens. + */ interface CertificateChain { /** * Common name of the leaf certificate. It must be set if the {@link #trustStoreFile} does not have @@ -196,18 +206,21 @@ interface CertificateChain { } /** - * Different options to configure authorization requests + * Configuration for managing an authorization code flow. */ + @ConfigDocSection Authentication authentication(); /** - * Authorization code grant configuration + * Configuration to complete an authorization code flow grant. */ + @ConfigDocSection CodeGrant codeGrant(); /** * Default token state manager configuration */ + @ConfigDocSection TokenStateManager tokenStateManager(); /** @@ -317,8 +330,9 @@ interface Backchannel { } /** - * Configuration for controlling how JsonWebKeySet containing verification keys should be acquired and managed. + * How JsonWebKey verification key set should be acquired and managed. */ + @ConfigDocSection Jwks jwks(); interface Jwks { diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index 5d34b1c7c4e693..4b1846f90a8578 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -96,6 +96,7 @@ AdditionalBeanBuildItem ensureProducerIsRetained() { return AdditionalBeanBuildItem.builder() .setUnremovable() .addBeanClasses( + AutoConfiguredOpenTelemetrySdkBuilderCustomizer.SimpleLogRecordProcessorCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracingResourceCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.SamplerCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracerProviderCustomizer.class, diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java index e33d6271665b41..6fc49278641f00 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java @@ -28,7 +28,7 @@ import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterConfigBuilder; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig; import io.quarkus.opentelemetry.runtime.exporter.otlp.OTelExporterRecorder; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundSpanProcessor; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.tls.TlsRegistryBuildItem; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; @@ -84,7 +84,8 @@ void config(BuildProducer runTimeConfigBuilderPro @BuildStep(onlyIf = OtlpExporterProcessor.OtlpTracingExporterEnabled.class) @Record(ExecutionTime.RUNTIME_INIT) @Consume(TlsRegistryBuildItem.class) - void createBatchSpanProcessor(OTelExporterRecorder recorder, + void createSpanProcessor(OTelExporterRecorder recorder, + OTelBuildConfig oTelBuildConfig, OTelRuntimeConfig otelRuntimeConfig, OtlpExporterRuntimeConfig exporterRuntimeConfig, CoreVertxBuildItem vertxBuildItem, @@ -95,7 +96,7 @@ void createBatchSpanProcessor(OTelExporterRecorder recorder, return; } syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem - .configure(LateBoundBatchSpanProcessor.class) + .configure(LateBoundSpanProcessor.class) .types(SpanProcessor.class) .setRuntimeInit() .scope(Singleton.class) @@ -103,8 +104,8 @@ void createBatchSpanProcessor(OTelExporterRecorder recorder, .addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class), new Type[] { ClassType.create(DotName.createSimple(SpanExporter.class.getName())) }, null)) .addInjectionPoint(ClassType.create(DotName.createSimple(TlsConfigurationRegistry.class))) - .createWith(recorder.batchSpanProcessorForOtlp(otelRuntimeConfig, exporterRuntimeConfig, - vertxBuildItem.getVertx())) + .createWith(recorder.spanProcessorForOtlp(oTelBuildConfig, otelRuntimeConfig, + exporterRuntimeConfig, vertxBuildItem.getVertx())) .done()); } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java index a547f90bef18f9..5c7faccb332579 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java @@ -12,7 +12,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundSpanProcessor; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.HttpInstrumenterVertxTracer; import io.quarkus.test.QuarkusUnitTest; import io.vertx.core.spi.observability.HttpRequest; @@ -26,7 +26,7 @@ public class OpenTelemetryDisabledSdkTest { .overrideConfigKey("quarkus.otel.sdk.disabled", "true"); @Inject - LateBoundBatchSpanProcessor batchSpanProcessor; + LateBoundSpanProcessor spanProcessor; @Inject OpenTelemetry openTelemetry; @@ -40,7 +40,7 @@ public class OpenTelemetryDisabledSdkTest { @Test void testNoTracer() { // The OTel API doesn't provide a clear way to check if a tracer is an effective NOOP tracer. - Assertions.assertTrue(batchSpanProcessor.isDelegateNull(), "BatchSpanProcessor delegate must not be set"); + Assertions.assertTrue(spanProcessor.isDelegateNull(), "SpanProcessor delegate must not be set"); } @Test diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java index 7d9e074025c181..1a0372f194396d 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java @@ -11,7 +11,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.sdk.metrics.export.MetricExporter; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundSpanProcessor; import io.quarkus.test.QuarkusUnitTest; public class OtlpTraceExporterDisabledTest { @@ -25,7 +25,7 @@ public class OtlpTraceExporterDisabledTest { OpenTelemetry openTelemetry; @Inject - Instance lateBoundBatchSpanProcessorInstance; + Instance lateBoundSpanProcessorInstance; @Inject Instance metricExporters; @@ -33,7 +33,7 @@ public class OtlpTraceExporterDisabledTest { @Test void testOpenTelemetryButNoBatchSpanProcessor() { assertNotNull(openTelemetry); - assertFalse(lateBoundBatchSpanProcessorInstance.isResolvable()); + assertFalse(lateBoundSpanProcessorInstance.isResolvable()); assertFalse(metricExporters.isResolvable()); } } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java index 86218be707ce6b..f9c9e4c1747721 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.lang.reflect.InvocationTargetException; +import java.util.Locale; import jakarta.inject.Inject; @@ -36,6 +37,7 @@ public class OpenTelemetrySamplerConfigTest { void test() throws NoSuchFieldException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { Sampler sampler = TestUtil.getSampler(openTelemetry); - assertEquals(String.format("TraceIdRatioBased{%.6f}", 0.5d), sampler.getDescription()); + // Fix the locale to ROOT, so we don't get 0,500000 + assertEquals(String.format(Locale.ROOT, "TraceIdRatioBased{%.6f}", 0.5d), sampler.getDescription()); } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java index e601962c7b01cc..eac3ccb8c1e89e 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java @@ -18,6 +18,10 @@ import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.logs.LogRecordProcessor; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.IdGenerator; @@ -27,7 +31,7 @@ import io.quarkus.arc.All; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundSpanProcessor; import io.quarkus.opentelemetry.runtime.propagation.TextMapPropagatorCustomizer; import io.quarkus.opentelemetry.runtime.tracing.DelayedAttributes; import io.quarkus.opentelemetry.runtime.tracing.DropTargetsSampler; @@ -39,6 +43,47 @@ public interface AutoConfiguredOpenTelemetrySdkBuilderCustomizer { void customize(AutoConfiguredOpenTelemetrySdkBuilder builder); + @Singleton + final class SimpleLogRecordProcessorCustomizer implements AutoConfiguredOpenTelemetrySdkBuilderCustomizer { + private SimpleLogRecordProcessorBiFunction biFunction; + + public SimpleLogRecordProcessorCustomizer( + OTelBuildConfig oTelBuildConfig, + Instance ilre) { + if (oTelBuildConfig.simple() && ilre.isResolvable()) { + LogRecordProcessor lrp = SimpleLogRecordProcessor.create(ilre.get()); + this.biFunction = new SimpleLogRecordProcessorBiFunction(lrp); + } + } + + @Override + public void customize(AutoConfiguredOpenTelemetrySdkBuilder builder) { + if (biFunction != null) { + builder.addLogRecordProcessorCustomizer(biFunction); + } + } + } + + class SimpleLogRecordProcessorBiFunction + implements BiFunction { + + private final LogRecordProcessor logRecordProcessor; + + public SimpleLogRecordProcessorBiFunction(LogRecordProcessor logRecordProcessor) { + this.logRecordProcessor = logRecordProcessor; + } + + @Override + public LogRecordProcessor apply(LogRecordProcessor lrp, ConfigProperties cp) { + // only change batch lrp, leave others + if (lrp instanceof BatchLogRecordProcessor) { + return logRecordProcessor; + } else { + return lrp; + } + } + } + @Singleton final class TracingResourceCustomizer implements AutoConfiguredOpenTelemetrySdkBuilderCustomizer { @@ -174,7 +219,7 @@ public SdkTracerProviderBuilder apply(SdkTracerProviderBuilder tracerProviderBui spanProcessors.stream().filter(new Predicate() { @Override public boolean test(SpanProcessor sp) { - return !(sp instanceof RemoveableLateBoundBatchSpanProcessor); + return !(sp instanceof RemoveableLateBoundSpanProcessor); } }) .forEach(tracerProviderBuilder::addSpanProcessor); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java index c724ea9d640e70..bd47ccc601ec8a 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java @@ -37,6 +37,17 @@ public interface OTelBuildConfig { @WithDefault("true") boolean enabled(); + /** + * Should we use simple processor for spans and log records. + * This will disable batch processing and the exporter will send + * telemetry data right away. + * This is recommended for serverless applications. + *

+ * Defaults to false. + */ + @WithDefault("false") + boolean simple(); + /** * Trace exporter configurations. */ diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java index 65f80415778bd7..57cd1f9d2253ed 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java @@ -36,8 +36,12 @@ import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessorBuilder; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; +import io.quarkus.opentelemetry.runtime.config.runtime.BatchSpanProcessorConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.*; import io.quarkus.opentelemetry.runtime.exporter.otlp.logs.NoopLogRecordExporter; @@ -48,8 +52,8 @@ import io.quarkus.opentelemetry.runtime.exporter.otlp.metrics.VertxHttpMetricsExporter; import io.quarkus.opentelemetry.runtime.exporter.otlp.sender.VertxGrpcSender; import io.quarkus.opentelemetry.runtime.exporter.otlp.sender.VertxHttpSender; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundSpanProcessor; import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.VertxGrpcSpanExporter; import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.VertxHttpSpanExporter; import io.quarkus.runtime.annotations.Recorder; @@ -68,23 +72,24 @@ public class OTelExporterRecorder { public static final String BASE2EXPONENTIAL_AGGREGATION_NAME = AggregationUtil .aggregationName(Aggregation.base2ExponentialBucketHistogram()); - public Function, LateBoundBatchSpanProcessor> batchSpanProcessorForOtlp( + public Function, LateBoundSpanProcessor> spanProcessorForOtlp( + OTelBuildConfig oTelBuildConfig, OTelRuntimeConfig otelRuntimeConfig, OtlpExporterRuntimeConfig exporterRuntimeConfig, Supplier vertx) { URI baseUri = getTracesUri(exporterRuntimeConfig); // do the creation and validation here in order to preserve backward compatibility return new Function<>() { @Override - public LateBoundBatchSpanProcessor apply( - SyntheticCreationalContext context) { + public LateBoundSpanProcessor apply( + SyntheticCreationalContext context) { if (otelRuntimeConfig.sdkDisabled() || baseUri == null) { - return RemoveableLateBoundBatchSpanProcessor.INSTANCE; + return RemoveableLateBoundSpanProcessor.INSTANCE; } // Only create the OtlpGrpcSpanExporter if an endpoint was set in runtime config and was properly validated at startup Instance spanExporters = context.getInjectedReference(new TypeLiteral<>() { }); if (!spanExporters.isUnsatisfied()) { - return RemoveableLateBoundBatchSpanProcessor.INSTANCE; + return RemoveableLateBoundSpanProcessor.INSTANCE; } try { @@ -93,15 +98,21 @@ public LateBoundBatchSpanProcessor apply( var spanExporter = createSpanExporter(exporterRuntimeConfig, vertx.get(), baseUri, tlsConfigurationRegistry); - BatchSpanProcessorBuilder processorBuilder = BatchSpanProcessor.builder(spanExporter); + if (oTelBuildConfig.simple()) { + SimpleSpanProcessorBuilder processorBuilder = SimpleSpanProcessor.builder(spanExporter); + return new LateBoundSpanProcessor(processorBuilder.build()); + } else { + BatchSpanProcessorBuilder processorBuilder = BatchSpanProcessor.builder(spanExporter); - processorBuilder.setScheduleDelay(otelRuntimeConfig.bsp().scheduleDelay()); - processorBuilder.setMaxQueueSize(otelRuntimeConfig.bsp().maxQueueSize()); - processorBuilder.setMaxExportBatchSize(otelRuntimeConfig.bsp().maxExportBatchSize()); - processorBuilder.setExporterTimeout(otelRuntimeConfig.bsp().exportTimeout()); - // processorBuilder.setMeterProvider() // TODO add meter provider to span processor. + BatchSpanProcessorConfig bspc = otelRuntimeConfig.bsp(); + processorBuilder.setScheduleDelay(bspc.scheduleDelay()); + processorBuilder.setMaxQueueSize(bspc.maxQueueSize()); + processorBuilder.setMaxExportBatchSize(bspc.maxExportBatchSize()); + processorBuilder.setExporterTimeout(bspc.exportTimeout()); + // processorBuilder.setMeterProvider() // TODO add meter provider to span processor. - return new LateBoundBatchSpanProcessor(processorBuilder.build()); + return new LateBoundSpanProcessor(processorBuilder.build()); + } } catch (IllegalArgumentException iae) { throw new IllegalStateException("Unable to install OTLP Exporter", iae); } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java similarity index 84% rename from extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java rename to extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java index 90318940e34973..94663996276c01 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java @@ -7,20 +7,19 @@ import io.opentelemetry.sdk.trace.ReadWriteSpan; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SpanProcessor; -import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; /** - * Class to facilitate a delay in when the worker thread inside {@link BatchSpanProcessor} + * Class to facilitate a delay in when the worker thread inside {@link SpanProcessor} * is started, enabling Quarkus to instantiate a {@link io.opentelemetry.api.trace.TracerProvider} - * during static initialization and set a {@link BatchSpanProcessor} delegate during runtime initialization. + * during static initialization and set a {@link SpanProcessor} delegate during runtime initialization. */ -public class LateBoundBatchSpanProcessor implements SpanProcessor { - private static final Logger log = Logger.getLogger(LateBoundBatchSpanProcessor.class); +public class LateBoundSpanProcessor implements SpanProcessor { + private static final Logger log = Logger.getLogger(LateBoundSpanProcessor.class); private boolean warningLogged = false; - private BatchSpanProcessor delegate; + private SpanProcessor delegate; - public LateBoundBatchSpanProcessor(BatchSpanProcessor delegate) { + public LateBoundSpanProcessor(SpanProcessor delegate) { this.delegate = delegate; } @@ -104,7 +103,7 @@ private void resetDelegate() { */ private void logDelegateNotFound() { if (!warningLogged) { - log.warn("No BatchSpanProcessor delegate specified, no action taken."); + log.warn("No SpanProcessor delegate specified, no action taken."); warningLogged = true; } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java deleted file mode 100644 index d8654e5ff634ef..00000000000000 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.quarkus.opentelemetry.runtime.exporter.otlp.tracing; - -import io.quarkus.opentelemetry.runtime.AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracerProviderCustomizer; - -/** - * The only point in having this class is to allow {@link TracerProviderCustomizer} - * to easily ignore the configured {@link LateBoundBatchSpanProcessor}. - */ -public final class RemoveableLateBoundBatchSpanProcessor extends LateBoundBatchSpanProcessor { - - public static final RemoveableLateBoundBatchSpanProcessor INSTANCE = new RemoveableLateBoundBatchSpanProcessor(); - - private RemoveableLateBoundBatchSpanProcessor() { - super(null); - } -} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundSpanProcessor.java new file mode 100644 index 00000000000000..9d04b984d7d513 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundSpanProcessor.java @@ -0,0 +1,16 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.tracing; + +import io.quarkus.opentelemetry.runtime.AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracerProviderCustomizer; + +/** + * The only point in having this class is to allow {@link TracerProviderCustomizer} + * to easily ignore the configured {@link LateBoundSpanProcessor}. + */ +public final class RemoveableLateBoundSpanProcessor extends LateBoundSpanProcessor { + + public static final RemoveableLateBoundSpanProcessor INSTANCE = new RemoveableLateBoundSpanProcessor(); + + private RemoveableLateBoundSpanProcessor() { + super(null); + } +} diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java index 81e27392847049..9c6dfe0f278148 100644 --- a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java +++ b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java @@ -5,6 +5,7 @@ import java.sql.Connection; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -55,6 +56,7 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.quartz.Nonconcurrent; import io.quarkus.quartz.runtime.QuarkusQuartzConnectionPoolProvider; import io.quarkus.quartz.runtime.QuartzBuildTimeConfig; import io.quarkus.quartz.runtime.QuartzExtensionPointConfig; @@ -69,6 +71,7 @@ import io.quarkus.quartz.runtime.jdbc.QuarkusStdJDBCDelegate; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.deployment.ScheduledBusinessMethodItem; import io.quarkus.scheduler.deployment.SchedulerImplementationBuildItem; public class QuartzProcessor { @@ -79,6 +82,7 @@ public class QuartzProcessor { private static final DotName DELEGATE_HSQLDB = DotName.createSimple(QuarkusHSQLDBDelegate.class.getName()); private static final DotName DELEGATE_MSSQL = DotName.createSimple(QuarkusMSSQLDelegate.class.getName()); private static final DotName DELEGATE_STDJDBC = DotName.createSimple(QuarkusStdJDBCDelegate.class.getName()); + private static final DotName NONCONCURRENT = DotName.createSimple(Nonconcurrent.class); @BuildStep FeatureBuildItem feature() { @@ -313,12 +317,23 @@ public void start(BuildProducer serviceStart, @Record(RUNTIME_INIT) public void quartzSupportBean(QuartzRuntimeConfig runtimeConfig, QuartzBuildTimeConfig buildTimeConfig, QuartzRecorder recorder, - BuildProducer syntheticBeanBuildItemBuildProducer, - QuartzJDBCDriverDialectBuildItem driverDialect) { + QuartzJDBCDriverDialectBuildItem driverDialect, + List scheduledMethods, + BuildProducer syntheticBeanBuildItemBuildProducer) { + + Set nonconcurrentMethods = new HashSet<>(); + for (ScheduledBusinessMethodItem m : scheduledMethods) { + if (m.getMethod().hasAnnotation(NONCONCURRENT)) { + nonconcurrentMethods.add(m.getMethod().declaringClass().name() + "#" + m.getMethod().name()); + } + } syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(QuartzSupport.class) .scope(Singleton.class) // this should be @ApplicationScoped but it fails for some reason .setRuntimeInit() - .supplier(recorder.quartzSupportSupplier(runtimeConfig, buildTimeConfig, driverDialect.getDriver())).done()); + .supplier(recorder.quartzSupportSupplier(runtimeConfig, buildTimeConfig, driverDialect.getDriver(), + nonconcurrentMethods)) + .done()); } + } diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentJobDefinitionTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentJobDefinitionTest.java new file mode 100644 index 00000000000000..6cb0446db35283 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentJobDefinitionTest.java @@ -0,0 +1,67 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class NonconcurrentJobDefinitionTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Jobs.class)) + .overrideConfigKey("quarkus.scheduler.start-mode", "forced"); + + @Inject + QuartzScheduler scheduler; + + @Test + public void testExecution() throws InterruptedException { + scheduler.newJob("foo") + .setTask(se -> { + Jobs.NONCONCURRENT_COUNTER.incrementAndGet(); + try { + if (!Jobs.CONCURRENT_LATCH.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("nonconcurrent() execution blocked too long..."); + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + if (Jobs.NONCONCURRENT_COUNTER.get() == 1) { + // concurrent() executed >= 5x and nonconcurrent() 1x + Jobs.NONCONCURRENT_LATCH.countDown(); + } + }) + .setInterval("1s") + .setNonconcurrent() + .schedule(); + + assertTrue(Jobs.NONCONCURRENT_LATCH.await(10, TimeUnit.SECONDS), + String.format("nonconcurrent() executed: %sx", Jobs.NONCONCURRENT_COUNTER.get())); + } + + static class Jobs { + + static final CountDownLatch NONCONCURRENT_LATCH = new CountDownLatch(1); + static final CountDownLatch CONCURRENT_LATCH = new CountDownLatch(5); + + static final AtomicInteger NONCONCURRENT_COUNTER = new AtomicInteger(0); + + @Scheduled(identity = "bar", every = "1s") + void concurrent() throws InterruptedException { + CONCURRENT_LATCH.countDown(); + } + + } + +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentOnQuartzThreadTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentOnQuartzThreadTest.java new file mode 100644 index 00000000000000..dee4f50683f48e --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentOnQuartzThreadTest.java @@ -0,0 +1,56 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.quartz.Nonconcurrent; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class NonconcurrentOnQuartzThreadTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Jobs.class)) + .overrideConfigKey("quarkus.quartz.run-blocking-scheduled-method-on-quartz-thread", + "true"); + + @Test + public void testExecution() throws InterruptedException { + assertTrue(Jobs.NONCONCURRENT_LATCH.await(10, TimeUnit.SECONDS), + String.format("nonconcurrent() executed: %sx", Jobs.NONCONCURRENT_COUNTER.get())); + } + + static class Jobs { + + static final CountDownLatch NONCONCURRENT_LATCH = new CountDownLatch(1); + static final CountDownLatch CONCURRENT_LATCH = new CountDownLatch(5); + + static final AtomicInteger NONCONCURRENT_COUNTER = new AtomicInteger(0); + + @Nonconcurrent + @Scheduled(identity = "foo", every = "1s") + void nonconcurrent() throws InterruptedException { + NONCONCURRENT_COUNTER.incrementAndGet(); + if (!CONCURRENT_LATCH.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("nonconcurrent() execution blocked too long..."); + } + if (NONCONCURRENT_COUNTER.get() == 1) { + // concurrent() executed >= 5x and nonconcurrent() 1x + NONCONCURRENT_LATCH.countDown(); + } + } + + @Scheduled(identity = "bar", every = "1s") + void concurrent() throws InterruptedException { + CONCURRENT_LATCH.countDown(); + } + + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentProgrammaticTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentProgrammaticTest.java new file mode 100644 index 00000000000000..5ebeb934053c7e --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentProgrammaticTest.java @@ -0,0 +1,91 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.test.QuarkusUnitTest; + +public class NonconcurrentProgrammaticTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Jobs.class)) + .overrideConfigKey("quarkus.scheduler.start-mode", "halted"); + + @Inject + QuartzScheduler scheduler; + + @Test + public void testExecution() throws SchedulerException, InterruptedException { + JobDetail job = JobBuilder.newJob(Jobs.class) + .withIdentity("foo", Scheduler.class.getName()) + .build(); + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity("foo", Scheduler.class.getName()) + .startNow() + .withSchedule(SimpleScheduleBuilder.simpleSchedule() + .withIntervalInSeconds(1) + .repeatForever()) + .build(); + scheduler.getScheduler().scheduleJob(job, trigger); + + scheduler.resume(); + + assertTrue(Jobs.NONCONCURRENT_LATCH.await(10, TimeUnit.SECONDS), + String.format("nonconcurrent() executed: %sx", Jobs.NONCONCURRENT_COUNTER.get())); + } + + @DisallowConcurrentExecution + static class Jobs implements Job { + + static final CountDownLatch NONCONCURRENT_LATCH = new CountDownLatch(1); + static final CountDownLatch CONCURRENT_LATCH = new CountDownLatch(5); + + static final AtomicInteger NONCONCURRENT_COUNTER = new AtomicInteger(0); + + @Scheduled(identity = "bar", every = "1s") + void concurrent() throws InterruptedException { + CONCURRENT_LATCH.countDown(); + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + Jobs.NONCONCURRENT_COUNTER.incrementAndGet(); + try { + if (!Jobs.CONCURRENT_LATCH.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("nonconcurrent() execution blocked too long..."); + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + if (Jobs.NONCONCURRENT_COUNTER.get() == 1) { + // concurrent() executed >= 5x and nonconcurrent() 1x + Jobs.NONCONCURRENT_LATCH.countDown(); + } + } + + } + +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentTest.java new file mode 100644 index 00000000000000..ec26ba9be0524c --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentTest.java @@ -0,0 +1,54 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.quartz.Nonconcurrent; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class NonconcurrentTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Jobs.class)); + + @Test + public void testExecution() throws InterruptedException { + assertTrue(Jobs.NONCONCURRENT_LATCH.await(10, TimeUnit.SECONDS), + String.format("nonconcurrent() executed: %sx", Jobs.NONCONCURRENT_COUNTER.get())); + } + + static class Jobs { + + static final CountDownLatch NONCONCURRENT_LATCH = new CountDownLatch(1); + static final CountDownLatch CONCURRENT_LATCH = new CountDownLatch(5); + + static final AtomicInteger NONCONCURRENT_COUNTER = new AtomicInteger(0); + + @Nonconcurrent + @Scheduled(identity = "foo", every = "1s") + void nonconcurrent() throws InterruptedException { + NONCONCURRENT_COUNTER.incrementAndGet(); + if (!CONCURRENT_LATCH.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("nonconcurrent() execution blocked too long..."); + } + if (NONCONCURRENT_COUNTER.get() == 1) { + // concurrent() executed >= 5x and nonconcurrent() 1x + NONCONCURRENT_LATCH.countDown(); + } + } + + @Scheduled(identity = "bar", every = "1s") + void concurrent() throws InterruptedException { + CONCURRENT_LATCH.countDown(); + } + + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/ProgrammaticJobsTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/ProgrammaticJobsTest.java index d2f5e62a5a55e7..aa027694004b93 100644 --- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/ProgrammaticJobsTest.java +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/ProgrammaticJobsTest.java @@ -69,7 +69,7 @@ public void testJobs() throws InterruptedException { .setSkipPredicate(AlwaysSkipPredicate.class) .schedule(); - Scheduler.JobDefinition job1 = scheduler.newJob("foo") + Scheduler.JobDefinition job1 = scheduler.newJob("foo") .setInterval("1s") .setTask(ec -> { assertTrue(Arc.container().requestContext().isActive()); @@ -79,7 +79,7 @@ public void testJobs() throws InterruptedException { assertEquals("Sync task was already set", assertThrows(IllegalStateException.class, () -> job1.setAsyncTask(ec -> null)).getMessage()); - Scheduler.JobDefinition job2 = scheduler.newJob("foo").setCron("0/5 * * * * ?"); + Scheduler.JobDefinition job2 = scheduler.newJob("foo").setCron("0/5 * * * * ?"); assertEquals("Either sync or async task must be set", assertThrows(IllegalStateException.class, () -> job2.schedule()).getMessage()); job2.setTask(ec -> { @@ -117,7 +117,7 @@ public void testJobs() throws InterruptedException { @Test public void testAsyncJob() throws InterruptedException, SchedulerException { String identity = "fooAsync"; - JobDefinition asyncJob = scheduler.newJob(identity) + JobDefinition asyncJob = scheduler.newJob(identity) .setInterval("1s") .setAsyncTask(ec -> { assertTrue(Context.isOnEventLoopThread() && VertxContext.isOnDuplicatedContext()); diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/Nonconcurrent.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/Nonconcurrent.java new file mode 100644 index 00000000000000..27bcaa2104b040 --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/Nonconcurrent.java @@ -0,0 +1,35 @@ +package io.quarkus.quartz; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.Job; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.SkippedExecution; + +/** + * A scheduled method annotated with this annotation may not be executed concurrently. The behavior is identical to a + * {@link Job} class annotated with {@link DisallowConcurrentExecution}. + *

+ * If {@code quarkus.quartz.run-blocking-scheduled-method-on-quartz-thread} is set to + * {@code false} the execution of a scheduled method is offloaded to a specific Quarkus thread pool but the triggering Quartz + * thread is blocked until the execution is finished. Therefore, make sure the Quartz thread pool is configured appropriately. + *

+ * If {@code quarkus.quartz.run-blocking-scheduled-method-on-quartz-thread} is set to {@code true} the scheduled method is + * invoked on a thread managed by Quartz. + *

+ * Unlike with {@link Scheduled.ConcurrentExecution#SKIP} the {@link SkippedExecution} event is never fired if a method + * execution is skipped by Quartz. + * + * @see DisallowConcurrentExecution + */ +@Target(METHOD) +@Retention(RUNTIME) +public @interface Nonconcurrent { + +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/QuartzScheduler.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/QuartzScheduler.java index 395a6de8369a4a..60c30ab3d7292e 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/QuartzScheduler.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/QuartzScheduler.java @@ -13,4 +13,18 @@ public interface QuartzScheduler extends Scheduler { */ org.quartz.Scheduler getScheduler(); + @Override + QuartzJobDefinition newJob(String identity); + + interface QuartzJobDefinition extends JobDefinition { + + /** + * + * @return self + * @see Nonconcurrent + */ + QuartzJobDefinition setNonconcurrent(); + + } + } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java index 9a1bd26cae449a..7ea820528fcd6b 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java @@ -1,6 +1,7 @@ package io.quarkus.quartz.runtime; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; import io.quarkus.runtime.annotations.Recorder; @@ -9,11 +10,11 @@ public class QuartzRecorder { public Supplier quartzSupportSupplier(QuartzRuntimeConfig runtimeConfig, - QuartzBuildTimeConfig buildTimeConfig, Optional driverDialect) { + QuartzBuildTimeConfig buildTimeConfig, Optional driverDialect, Set nonconcurrentMethods) { return new Supplier() { @Override public QuartzSupport get() { - return new QuartzSupport(runtimeConfig, buildTimeConfig, driverDialect); + return new QuartzSupport(runtimeConfig, buildTimeConfig, driverDialect, nonconcurrentMethods); } }; } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java index e67a8c9f41ac95..a231c29e65b61b 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java @@ -13,9 +13,11 @@ import java.util.Properties; import java.util.Set; import java.util.TimeZone; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -40,6 +42,7 @@ import org.jboss.logging.Logger; import org.quartz.CronScheduleBuilder; +import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobBuilder; import org.quartz.JobDetail; @@ -60,6 +63,7 @@ import org.quartz.spi.TriggerFiredBundle; import io.quarkus.arc.Subclass; +import io.quarkus.quartz.Nonconcurrent; import io.quarkus.quartz.QuartzScheduler; import io.quarkus.runtime.StartupEvent; import io.quarkus.scheduler.DelayedExecution; @@ -223,7 +227,8 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) { invoker.isBlocking() && runtimeConfig.runBlockingScheduledMethodOnQuartzThread, SchedulerUtils.parseExecutionMaxDelayAsMillis(scheduled), blockingExecutor); - JobDetail jobDetail = createJobDetail(identity, method.getInvokerClassName()); + JobDetail jobDetail = createJobBuilder(identity, method.getInvokerClassName(), + quartzSupport.isNonconcurrent(method)).build(); Optional> triggerBuilder = createTrigger(identity, scheduled, runtimeConfig, jobDetail); @@ -471,7 +476,7 @@ public Trigger getScheduledJob(String identity) { } @Override - public JobDefinition newJob(String identity) { + public QuartzJobDefinition newJob(String identity) { if (!isStarted()) { throw notStarted(); } @@ -479,7 +484,7 @@ public JobDefinition newJob(String identity) { if (scheduledTasks.containsKey(identity)) { throw new IllegalStateException("A job with this identity is already scheduled: " + identity); } - return new QuartzJobDefinition(identity); + return new QuartzJobDefinitionImpl(identity); } @Override @@ -582,13 +587,15 @@ private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSuppo props.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, "false"); props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, buildTimeConfig.storeType.clazz); + // The org.quartz.jobStore.misfireThreshold can be used for all supported job stores + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", + "" + runtimeConfig.misfireThreshold.toMillis()); + if (buildTimeConfig.storeType.isDbStore()) { String dataSource = buildTimeConfig.dataSourceName.orElse("QUARKUS_QUARTZ_DEFAULT_DATASOURCE"); QuarkusQuartzConnectionPoolProvider.setDataSourceName(dataSource); boolean serializeJobData = buildTimeConfig.serializeJobData.orElse(false); props.put(StdSchedulerFactory.PROP_JOB_STORE_USE_PROP, serializeJobData ? "false" : "true"); - props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", - "" + runtimeConfig.misfireThreshold.toMillis()); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".tablePrefix", buildTimeConfig.tablePrefix); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".dataSource", dataSource); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".driverDelegateClass", @@ -687,13 +694,15 @@ StartMode initStartMode(SchedulerRuntimeConfig schedulerRuntimeConfig, QuartzRun } } - private JobDetail createJobDetail(String identity, String invokerClassName) { - return JobBuilder.newJob(InvokerJob.class) + private JobBuilder createJobBuilder(String identity, String invokerClassName, boolean noncurrent) { + Class jobClass = noncurrent ? NonconcurrentInvokerJob.class + : InvokerJob.class; + return JobBuilder.newJob(jobClass) // new JobKey(identity, "io.quarkus.scheduler.Scheduler") .withIdentity(identity, Scheduler.class.getName()) // this info is redundant but keep it for backward compatibility .usingJobData(INVOKER_KEY, invokerClassName) - .requestRecovery().build(); + .requestRecovery(); } /** @@ -815,12 +824,26 @@ private Optional> createTrigger(String identity, Scheduled sch return Optional.of(triggerBuilder); } - class QuartzJobDefinition extends AbstractJobDefinition implements ExecutionMetadata { + class QuartzJobDefinitionImpl extends AbstractJobDefinition + implements ExecutionMetadata, QuartzJobDefinition { + + private boolean nonconcurrent; - QuartzJobDefinition(String id) { + QuartzJobDefinitionImpl(String id) { super(id); } + @Override + public QuartzJobDefinition setNonconcurrent() { + nonconcurrent = true; + return self(); + } + + @Override + public boolean nonconcurrent() { + return nonconcurrent; + } + @Override public boolean isRunOnVirtualThread() { return runOnVirtualThread; @@ -857,7 +880,7 @@ public Class skipPredicateClass() { } @Override - public JobDefinition setSkipPredicate(SkipPredicate skipPredicate) { + public QuartzJobDefinition setSkipPredicate(SkipPredicate skipPredicate) { if (storeType.isDbStore() && skipPredicateClass == null) { throw new IllegalStateException( "A skip predicate instance cannot be scheduled programmatically if DB store type is used; register a skip predicate class instead"); @@ -866,7 +889,7 @@ public JobDefinition setSkipPredicate(SkipPredicate skipPredicate) { } @Override - public JobDefinition setTask(Consumer task, boolean runOnVirtualThread) { + public QuartzJobDefinition setTask(Consumer task, boolean runOnVirtualThread) { if (storeType.isDbStore() && taskClass == null) { throw new IllegalStateException( "A task instance cannot be scheduled programmatically if DB store type is used; register a task class instead"); @@ -875,7 +898,7 @@ public JobDefinition setTask(Consumer task, boolean runOnVir } @Override - public JobDefinition setAsyncTask(Function> asyncTask) { + public QuartzJobDefinition setAsyncTask(Function> asyncTask) { if (storeType.isDbStore() && asyncTaskClass == null) { throw new IllegalStateException( "An async task instance cannot be scheduled programmatically if DB store type is used; register an async task class instead"); @@ -912,12 +935,15 @@ interface ExecutionMetadata { SkipPredicate skipPredicate(); Class skipPredicateClass(); + + boolean nonconcurrent(); } static final String SCHEDULED_METADATA = "scheduled_metadata"; static final String EXECUTION_METADATA_TASK_CLASS = "execution_metadata_task_class"; static final String EXECUTION_METADATA_ASYNC_TASK_CLASS = "execution_metadata_async_task_class"; static final String EXECUTION_METADATA_RUN_ON_VIRTUAL_THREAD = "execution_metadata_run_on_virtual_thread"; + static final String EXECUTION_METADATA_NONCONCURRENT = "execution_metadata_nonconcurrent"; static final String EXECUTION_METADATA_SKIP_PREDICATE_CLASS = "execution_metadata_skip_predicate_class"; QuartzTrigger createJobDefinitionQuartzTrigger(ExecutionMetadata executionMetadata, SyntheticScheduled scheduled, @@ -966,11 +992,8 @@ public boolean isBlocking() { }; } - JobBuilder jobBuilder = JobBuilder.newJob(InvokerJob.class) - // new JobKey(identity, "io.quarkus.scheduler.Scheduler") - .withIdentity(scheduled.identity(), Scheduler.class.getName()) - // this info is redundant but keep it for backward compatibility - .usingJobData(INVOKER_KEY, QuartzSchedulerImpl.class.getName()); + JobBuilder jobBuilder = createJobBuilder(scheduled.identity(), QuartzSchedulerImpl.class.getName(), + executionMetadata.nonconcurrent()); if (storeType.isDbStore()) { jobBuilder.usingJobData(SCHEDULED_METADATA, scheduled.toJson()) .usingJobData(EXECUTION_METADATA_RUN_ON_VIRTUAL_THREAD, Boolean.toString(runOnVirtualThread)); @@ -1045,6 +1068,23 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) { return quartzTrigger; } + /** + * @see Nonconcurrent + */ + @DisallowConcurrentExecution + static class NonconcurrentInvokerJob extends InvokerJob { + + NonconcurrentInvokerJob(QuartzTrigger trigger, Vertx vertx) { + super(trigger, vertx); + } + + @Override + boolean awaitResult() { + return true; + } + + } + /** * Although this class is not part of the public API it must not be renamed in order to preserve backward compatibility. The * name of this class can be stored in a Quartz table in the database. See https://github.com/quarkusio/quarkus/issues/29177 @@ -1060,11 +1100,24 @@ static class InvokerJob implements Job { this.vertx = vertx; } + boolean awaitResult() { + return false; + } + @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { if (trigger != null && trigger.invoker != null) { // could be null from previous runs try { - trigger.invoker.invoke(new QuartzScheduledExecution(trigger, jobExecutionContext)); + CompletionStage ret = trigger.invoker + .invoke(new QuartzScheduledExecution(trigger, jobExecutionContext)); + if (awaitResult()) { + try { + ret.toCompletableFuture().get(); + } catch (ExecutionException | CancellationException e) { + LOGGER.warnf("Unable to retrieve result for job %s: %s", + jobExecutionContext.getJobDetail().getKey().getName(), e.toString()); + } + } } catch (Exception e) { // already logged by the StatusEmitterInvoker } @@ -1190,6 +1243,9 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler Scheduler) thr // This is a job backed by a @Scheduled method or a JobDefinition return new InvokerJob(scheduledTasks.get(bundle.getJobDetail().getKey().getName()), vertx); } + if (jobClass.equals(NonconcurrentInvokerJob.class)) { + return new NonconcurrentInvokerJob(scheduledTasks.get(bundle.getJobDetail().getKey().getName()), vertx); + } if (Subclass.class.isAssignableFrom(jobClass)) { // Get the original class from an intercepted bean class jobClass = (Class) jobClass.getSuperclass(); @@ -1218,6 +1274,7 @@ static class SerializedExecutionMetadata implements ExecutionMetadata { private final Class>> asyncTaskClass; private final boolean runOnVirtualThread; private final Class skipPredicateClass; + private final boolean nonconcurrent; @SuppressWarnings("unchecked") public SerializedExecutionMetadata(JobDetail jobDetail) { @@ -1249,6 +1306,7 @@ public SerializedExecutionMetadata(JobDetail jobDetail) { } this.runOnVirtualThread = Boolean .parseBoolean(jobDetail.getJobDataMap().getString(EXECUTION_METADATA_RUN_ON_VIRTUAL_THREAD)); + this.nonconcurrent = Boolean.parseBoolean(jobDetail.getJobDataMap().getString(EXECUTION_METADATA_NONCONCURRENT)); } @Override @@ -1271,6 +1329,11 @@ public Class>> asyncTaskClass() return asyncTaskClass; } + @Override + public boolean nonconcurrent() { + return nonconcurrent; + } + @Override public boolean isRunOnVirtualThread() { return runOnVirtualThread; diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java index b343422373b788..18944bba970419 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java @@ -1,18 +1,25 @@ package io.quarkus.quartz.runtime; import java.util.Optional; +import java.util.Set; + +import io.quarkus.quartz.Nonconcurrent; +import io.quarkus.scheduler.common.runtime.ScheduledMethod; public class QuartzSupport { private final QuartzRuntimeConfig runtimeConfig; private final QuartzBuildTimeConfig buildTimeConfig; private final Optional driverDialect; + // # + private final Set nonconcurrentMethods; public QuartzSupport(QuartzRuntimeConfig runtimeConfig, QuartzBuildTimeConfig buildTimeConfig, - Optional driverDialect) { + Optional driverDialect, Set nonconcurrentMethods) { this.runtimeConfig = runtimeConfig; this.buildTimeConfig = buildTimeConfig; this.driverDialect = driverDialect; + this.nonconcurrentMethods = Set.copyOf(nonconcurrentMethods); } public QuartzRuntimeConfig getRuntimeConfig() { @@ -26,4 +33,14 @@ public QuartzBuildTimeConfig getBuildTimeConfig() { public Optional getDriverDialect() { return driverDialect; } + + /** + * + * @param method + * @return {@code true} if the scheduled method is annotated with {@link Nonconcurrent} + */ + public boolean isNonconcurrent(ScheduledMethod method) { + return nonconcurrentMethods.contains(method.getMethodDescription()); + } + } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java index 56809719f7b0af..725999bbd0b9a3 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java @@ -18,15 +18,17 @@ public final class MessageBundleMethodBuildItem extends MultiBuildItem { private final MethodInfo method; private final String template; private final boolean isDefaultBundle; + private final boolean hasGeneratedTemplate; MessageBundleMethodBuildItem(String bundleName, String key, String templateId, MethodInfo method, String template, - boolean isDefaultBundle) { + boolean isDefaultBundle, boolean hasGeneratedTemplate) { this.bundleName = bundleName; this.key = key; this.templateId = templateId; this.method = method; this.template = template; this.isDefaultBundle = isDefaultBundle; + this.hasGeneratedTemplate = hasGeneratedTemplate; } public String getBundleName() { @@ -54,6 +56,11 @@ public MethodInfo getMethod() { return method; } + /** + * + * @return {@code true} if there is a corresponding method declared on the message bundle interface + * @see #getMethod() + */ public boolean hasMethod() { return method != null; } @@ -79,6 +86,14 @@ public boolean isDefaultBundle() { return isDefaultBundle; } + /** + * + * @return {@code true} if the template was generated, e.g. a message bundle method for an enum + */ + public boolean hasGeneratedTemplate() { + return hasGeneratedTemplate; + } + /** * * @return the path diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index f67dd11dbc181f..5842bbd715abcb 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -702,8 +702,22 @@ void generateExamplePropertiesFiles(List messageBu List messages = entry.getValue(); messages.sort(Comparator.comparing(MessageBundleMethodBuildItem::getKey)); Path exampleProperties = generatedExamplesDir.resolve(entry.getKey() + ".properties"); - Files.write(exampleProperties, - messages.stream().map(m -> m.getMethod().name() + "=" + m.getTemplate()).collect(Collectors.toList())); + List lines = new ArrayList<>(); + for (MessageBundleMethodBuildItem m : messages) { + if (m.hasMethod()) { + if (m.hasGeneratedTemplate()) { + // Skip messages with generated templates + continue; + } + // Keys are mapped to method names + lines.add(m.getMethod().name() + "=" + m.getTemplate()); + } else { + // No corresponding method declared - use the key instead + // For example, there is no method for generated enum constant message keys + lines.add(m.getKey() + "=" + m.getTemplate()); + } + } + Files.write(exampleProperties, lines); } } @@ -992,6 +1006,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d } keyMap.put(key, new SimpleMessageMethod(method)); + boolean generatedTemplate = false; String messageTemplate = messageTemplates.get(method.name()); if (messageTemplate == null) { messageTemplate = getMessageAnnotationValue(messageAnnotation); @@ -1043,6 +1058,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d } generatedMessageTemplate.append("{/when}"); messageTemplate = generatedMessageTemplate.toString(); + generatedTemplate = true; } } } @@ -1068,7 +1084,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d } MessageBundleMethodBuildItem messageBundleMethod = new MessageBundleMethodBuildItem(bundleName, key, templateId, - method, messageTemplate, defaultBundleInterface == null); + method, messageTemplate, defaultBundleInterface == null, generatedTemplate); messageTemplateMethods .produce(messageBundleMethod); @@ -1139,8 +1155,7 @@ private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, Strin } MessageBundleMethodBuildItem messageBundleMethod = new MessageBundleMethodBuildItem(bundleName, enumConstantKey, - templateId, null, messageTemplate, - defaultBundleInterface == null); + templateId, null, messageTemplate, defaultBundleInterface == null, true); messageTemplateMethods.produce(messageBundleMethod); MethodCreator enumConstantMethod = bundleCreator.getMethodCreator(enumConstantKey, diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java new file mode 100644 index 00000000000000..4baf3b07566163 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java @@ -0,0 +1,31 @@ +package io.quarkus.qute.deployment; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.qute.runtime.devmode.QuteErrorPageSetup; + +@BuildSteps(onlyIf = IsDevelopment.class) +public class QuteDevModeProcessor { + + @BuildStep + void collectGeneratedContents(List templatePaths, + BuildProducer errors) { + Map contents = new HashMap<>(); + for (TemplatePathBuildItem template : templatePaths) { + if (!template.isFileBased()) { + contents.put(template.getPath(), template.getContent()); + } + } + // Set the global that could be used at runtime when a qute error page is rendered + DevConsoleManager.setGlobal(QuteErrorPageSetup.GENERATED_CONTENTS, contents); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumExampleFileTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumExampleFileTest.java new file mode 100644 index 00000000000000..008a289fa63406 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumExampleFileTest.java @@ -0,0 +1,64 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Properties; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class MessageBundleEnumExampleFileTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot(root -> root + .addClasses(Messages.class, MyEnum.class) + .addAsResource(new StringAsset(""" + myEnum_ON=On + myEnum_OFF=Off + myEnum_UNDEFINED=Undefined + """), + "messages/enu.properties")); + + @ProdBuildResults + ProdModeTestResults testResults; + + @Test + public void testExampleProperties() throws FileNotFoundException, IOException { + Path path = testResults.getBuildDir().resolve("qute-i18n-examples").resolve("enu.properties"); + assertTrue(path.toFile().canRead()); + Properties props = new Properties(); + props.load(new FileInputStream(path.toFile())); + assertEquals(3, props.size()); + assertTrue(props.containsKey("myEnum_ON")); + assertTrue(props.containsKey("myEnum_OFF")); + assertTrue(props.containsKey("myEnum_UNDEFINED")); + } + + @MessageBundle(value = "enu", locale = "en") + public interface Messages { + + // Replaced with: + // @Message("{#when myEnum}" + // + "{#is ON}{enu:myEnum_ON}" + // + "{#is OFF}{enu:myEnum_OFF}" + // + "{#is UNDEFINED}{enu:myEnum_UNDEFINED}" + // + "{/when}") + @Message + String myEnum(MyEnum myEnum); + + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java index 93c5fbe6b1327c..b8b8a43ae5955e 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java @@ -27,8 +27,7 @@ * There is a convenient way to localize enums. *

* If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined then - * it - * receives a generated template: + * it receives a generated template: * *

  * {#when enumParamName}
diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java
index b7ced362defac2..916522c98443f2 100644
--- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java
+++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java
@@ -2,6 +2,7 @@
 
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.StringReader;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.nio.file.Files;
@@ -12,6 +13,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.stream.Collectors;
@@ -19,6 +21,7 @@
 import org.jboss.logging.Logger;
 
 import io.quarkus.dev.ErrorPageGenerators;
+import io.quarkus.dev.console.DevConsoleManager;
 import io.quarkus.dev.spi.HotReplacementContext;
 import io.quarkus.dev.spi.HotReplacementSetup;
 import io.quarkus.qute.Engine;
@@ -33,6 +36,8 @@ public class QuteErrorPageSetup implements HotReplacementSetup {
 
     private static final Logger LOG = Logger.getLogger(QuteErrorPageSetup.class);
 
+    public static final String GENERATED_CONTENTS = "io.quarkus.qute.generatedContents";
+
     private static final String TEMPLATE_EXCEPTION = "io.quarkus.qute.TemplateException";
     private static final String ORIGIN = "io.quarkus.qute.TemplateNode$Origin";
 
@@ -139,6 +144,10 @@ String getProblemInfo(int index, Throwable problem, Template problemTemplate, Es
             LOG.warn("Unable to read the template source: " + templateId, e);
         }
 
+        if (sourceLines.isEmpty()) {
+            return Arrays.stream(messageLines).collect(Collectors.joining("
")); + } + List realLines = new ArrayList<>(); boolean endLinesSkipped = false; if (sourceLines.size() > 15) { @@ -187,6 +196,14 @@ private BufferedReader getBufferedReader(String templateId) throws IOException { } } } + // Source file not available - try to search the generated contents + Map generatedContents = DevConsoleManager.getGlobal(GENERATED_CONTENTS); + if (generatedContents != null) { + String template = generatedContents.get(templateId); + if (template != null) { + return new BufferedReader(new StringReader(template)); + } + } throw new IllegalStateException("Template source not available"); } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java index fa1fdb002bf0a8..ebc47ab056de65 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java @@ -239,18 +239,18 @@ private boolean deserializeObject(ClassInfo classInfo, ResultHandle objHandle, C ResultHandle nextField = loopCreator .invokeInterfaceMethod(ofMethod(Iterator.class, "next", Object.class), fieldsIterator); ResultHandle mapEntry = loopCreator.checkCast(nextField, Map.Entry.class); - ResultHandle fieldName = loopCreator - .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getKey", Object.class), mapEntry); ResultHandle fieldValue = loopCreator.checkCast(loopCreator .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getValue", Object.class), mapEntry), JsonNode.class); - loopCreator.ifTrue(loopCreator.invokeVirtualMethod(ofMethod(JsonNode.class, "isNull", boolean.class), fieldValue)) - .trueBranch().continueScope(loopCreator); + BytecodeCreator fieldReader = loopCreator + .ifTrue(loopCreator.invokeVirtualMethod(ofMethod(JsonNode.class, "isNull", boolean.class), fieldValue)) + .falseBranch(); + + ResultHandle fieldName = fieldReader + .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getKey", Object.class), mapEntry); + Switch.StringSwitch strSwitch = fieldReader.stringSwitch(fieldName); - Set deserializedFields = new HashSet<>(); - ResultHandle deserializationContext = deserialize.getMethodParam(1); - Switch.StringSwitch strSwitch = loopCreator.stringSwitch(fieldName); - return deserializeFields(classCreator, classInfo, deserializationContext, objHandle, fieldValue, deserializedFields, + return deserializeFields(classCreator, classInfo, deserialize.getMethodParam(1), objHandle, fieldValue, new HashSet<>(), strSwitch, parseTypeParameters(classInfo, classCreator)); } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomModuleLiveReloadTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomModuleLiveReloadTest.java new file mode 100644 index 00000000000000..b8db1861269700 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomModuleLiveReloadTest.java @@ -0,0 +1,144 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; + +import java.io.IOException; + +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import io.quarkus.jackson.ObjectMapperCustomizer; +import io.quarkus.test.QuarkusDevModeTest; +import io.vertx.core.json.JsonArray; + +public class CustomModuleLiveReloadTest { + + @RegisterExtension + static final QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(Resource.class, StringAndInt.class, StringAndIntSerializer.class, + StringAndIntDeserializer.class, Customizer.class) + .addAsResource(new StringAsset("index content"), "META-INF/resources/index.html")); + + @Test + void test() { + assertResponse(); + + // force reload + TEST.addResourceFile("META-INF/resources/index.html", "html content"); + + assertResponse(); + } + + private static void assertResponse() { + given().accept("application/json").get("test/array") + .then() + .statusCode(200) + .body(containsString("first:1"), containsString("second:2")); + } + + @Path("test") + public static class Resource { + + @Path("array") + @GET + @Produces(MediaType.APPLICATION_JSON) + public JsonArray array() { + var array = new JsonArray(); + array.add(new StringAndInt("first", 1)); + array.add(new StringAndInt("second", 2)); + return array; + } + } + + public static class StringAndInt { + private final String stringValue; + private final int intValue; + + public StringAndInt(String s, int i) { + this.stringValue = s; + this.intValue = i; + } + + public static StringAndInt parse(String value) { + if (value == null) { + return null; + } + int dot = value.indexOf(':'); + if (-1 == dot) { + throw new IllegalArgumentException(value); + } + try { + return new StringAndInt(value.substring(0, dot), Integer.parseInt(value.substring(dot + 1))); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(value, e); + } + } + + public String format() { + return this.stringValue + ":" + intValue; + } + } + + public static class StringAndIntSerializer extends StdSerializer { + + public StringAndIntSerializer() { + super(StringAndInt.class); + } + + @Override + public void serialize(StringAndInt value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if (value == null) + gen.writeNull(); + else { + gen.writeString(value.format()); + } + } + } + + public static class StringAndIntDeserializer extends StdDeserializer { + + public StringAndIntDeserializer() { + super(StringAndInt.class); + } + + @Override + public StringAndInt deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.currentToken() == JsonToken.VALUE_STRING) { + return StringAndInt.parse(p.getText()); + } else if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + return null; + } + } + + @Singleton + public static class Customizer implements ObjectMapperCustomizer { + @Override + public void customize(ObjectMapper objectMapper) { + var m = new SimpleModule("test"); + m.addSerializer(StringAndInt.class, new StringAndIntSerializer()); + m.addDeserializer(StringAndInt.class, new StringAndIntDeserializer()); + objectMapper.registerModule(m); + } + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java new file mode 100644 index 00000000000000..6bc5bda55d642c --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import java.util.Map; + +public class MapWrapper { + + private String name; + private Map properties; + + public MapWrapper() { + } + + public MapWrapper(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 855c43625c09e8..861f01ce08a962 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -123,6 +123,13 @@ public StateRecord echoDog(StateRecord stateRecord) { return stateRecord; } + @POST + @Path("/null-map-echo") + @Consumes(MediaType.APPLICATION_JSON) + public MapWrapper echoNullMap(MapWrapper mapWrapper) { + return mapWrapper; + } + @EnableSecureSerialization @GET @Path("/abstract-cat") diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index d2f22569f9a7a8..a5fa4d498c9232 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -36,7 +36,7 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class, StateRecord.class) + NestedInterface.class, StateRecord.class, MapWrapper.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n"), "application.properties"); @@ -733,4 +733,18 @@ public void testRecordEcho() { assertTrue(first >= 0); assertEquals(first, last); } + + @Test + public void testNullMapEcho() { + RestAssured + .with() + .body(new MapWrapper("test")) + .contentType("application/json; charset=utf-8") + .post("/simple/null-map-echo") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("test")) + .body("properties", Matchers.nullValue()); + } } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java index 65dec05aa59a4c..10ea3d373ce91f 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java @@ -25,7 +25,7 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class, StateRecord.class) + NestedInterface.class, StateRecord.class, MapWrapper.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n" + diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java index d0d2467c160ccf..97dbebb4014c54 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java @@ -103,7 +103,7 @@ public interface Scheduler { * @see Scheduled#identity() * @throws UnsupportedOperationException If the scheduler was not started */ - JobDefinition newJob(String identity); + JobDefinition newJob(String identity); /** * Removes the job previously added via {@link #newJob(String)}. @@ -130,7 +130,7 @@ public interface Scheduler { *

* The implementation is not thread-safe and should not be reused. */ - interface JobDefinition { + interface JobDefinition> { /** * The schedule is defined either by {@link #setCron(String)} or by {@link #setInterval(String)}. If both methods are @@ -142,7 +142,7 @@ interface JobDefinition { * @return self * @see Scheduled#cron() */ - JobDefinition setCron(String cron); + THIS setCron(String cron); /** * The schedule is defined either by {@link #setCron(String)} or by {@link #setInterval(String)}. If both methods are @@ -157,7 +157,7 @@ interface JobDefinition { * @return self * @see Scheduled#every() */ - JobDefinition setInterval(String every); + THIS setInterval(String every); /** * {@link Scheduled#delayed()} @@ -166,7 +166,7 @@ interface JobDefinition { * @return self * @see Scheduled#delayed() */ - JobDefinition setDelayed(String period); + THIS setDelayed(String period); /** * {@link Scheduled#concurrentExecution()} @@ -175,7 +175,7 @@ interface JobDefinition { * @return self * @see Scheduled#concurrentExecution() */ - JobDefinition setConcurrentExecution(ConcurrentExecution concurrentExecution); + THIS setConcurrentExecution(ConcurrentExecution concurrentExecution); /** * {@link Scheduled#skipExecutionIf()} @@ -184,7 +184,7 @@ interface JobDefinition { * @return self * @see Scheduled#skipExecutionIf() */ - JobDefinition setSkipPredicate(SkipPredicate skipPredicate); + THIS setSkipPredicate(SkipPredicate skipPredicate); /** * {@link Scheduled#skipExecutionIf()} @@ -193,7 +193,7 @@ interface JobDefinition { * @return self * @see Scheduled#skipExecutionIf() */ - JobDefinition setSkipPredicate(Class skipPredicateClass); + THIS setSkipPredicate(Class skipPredicateClass); /** * {@link Scheduled#overdueGracePeriod()} @@ -202,7 +202,7 @@ interface JobDefinition { * @return self * @see Scheduled#overdueGracePeriod() */ - JobDefinition setOverdueGracePeriod(String period); + THIS setOverdueGracePeriod(String period); /** * {@link Scheduled#timeZone()} @@ -210,7 +210,7 @@ interface JobDefinition { * @return self * @see Scheduled#timeZone() */ - JobDefinition setTimeZone(String timeZone); + THIS setTimeZone(String timeZone); /** * {@link Scheduled#executeWith()} @@ -220,7 +220,7 @@ interface JobDefinition { * @throws IllegalArgumentException If the composite scheduler is used and the selected implementation is not available * @see Scheduled#executeWith() */ - JobDefinition setExecuteWith(String implementation); + THIS setExecuteWith(String implementation); /** * {@link Scheduled#executionMaxDelay()} @@ -229,14 +229,14 @@ interface JobDefinition { * @return self * @see Scheduled#executionMaxDelay() */ - JobDefinition setExecutionMaxDelay(String maxDelay); + THIS setExecutionMaxDelay(String maxDelay); /** * * @param task * @return self */ - default JobDefinition setTask(Consumer task) { + default THIS setTask(Consumer task) { return setTask(task, false); } @@ -256,7 +256,7 @@ default JobDefinition setTask(Consumer task) { * @param taskClass * @return self */ - default JobDefinition setTask(Class> taskClass) { + default THIS setTask(Class> taskClass) { return setTask(taskClass, false); } @@ -267,7 +267,7 @@ default JobDefinition setTask(Class> task * @param runOnVirtualThread whether the task must be run on a virtual thread if the JVM allows it. * @return self */ - JobDefinition setTask(Consumer task, boolean runOnVirtualThread); + THIS setTask(Consumer task, boolean runOnVirtualThread); /** * The class must either represent a CDI bean or declare a public no-args constructor. @@ -286,14 +286,14 @@ default JobDefinition setTask(Class> task * @param runOnVirtualThread * @return self */ - JobDefinition setTask(Class> consumerClass, boolean runOnVirtualThread); + THIS setTask(Class> consumerClass, boolean runOnVirtualThread); /** * * @param asyncTask * @return self */ - JobDefinition setAsyncTask(Function> asyncTask); + THIS setAsyncTask(Function> asyncTask); /** * The class must either represent a CDI bean or declare a public no-args constructor. @@ -311,7 +311,7 @@ default JobDefinition setTask(Class> task * @param asyncTaskClass * @return self */ - JobDefinition setAsyncTask(Class>> asyncTaskClass); + THIS setAsyncTask(Class>> asyncTaskClass); /** * Attempts to schedule the job. diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java index d94f1c612a378d..d7a391628b7dd3 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java @@ -12,7 +12,7 @@ import io.quarkus.scheduler.common.runtime.util.SchedulerUtils; import io.smallrye.mutiny.Uni; -public abstract class AbstractJobDefinition implements JobDefinition { +public abstract class AbstractJobDefinition> implements JobDefinition { protected final String identity; protected String cron = ""; @@ -37,104 +37,104 @@ public AbstractJobDefinition(String identity) { } @Override - public JobDefinition setCron(String cron) { + public THIS setCron(String cron) { checkScheduled(); this.cron = Objects.requireNonNull(cron); - return this; + return self(); } @Override - public JobDefinition setInterval(String every) { + public THIS setInterval(String every) { checkScheduled(); this.every = Objects.requireNonNull(every); - return this; + return self(); } @Override - public JobDefinition setDelayed(String period) { + public THIS setDelayed(String period) { checkScheduled(); this.delayed = Objects.requireNonNull(period); - return this; + return self(); } @Override - public JobDefinition setConcurrentExecution(ConcurrentExecution concurrentExecution) { + public THIS setConcurrentExecution(ConcurrentExecution concurrentExecution) { checkScheduled(); this.concurrentExecution = Objects.requireNonNull(concurrentExecution); - return this; + return self(); } @Override - public JobDefinition setSkipPredicate(SkipPredicate skipPredicate) { + public THIS setSkipPredicate(SkipPredicate skipPredicate) { checkScheduled(); this.skipPredicate = Objects.requireNonNull(skipPredicate); - return this; + return self(); } @Override - public JobDefinition setSkipPredicate(Class skipPredicateClass) { + public THIS setSkipPredicate(Class skipPredicateClass) { checkScheduled(); this.skipPredicateClass = Objects.requireNonNull(skipPredicateClass); return setSkipPredicate(SchedulerUtils.instantiateBeanOrClass(skipPredicateClass)); } @Override - public JobDefinition setOverdueGracePeriod(String period) { + public THIS setOverdueGracePeriod(String period) { checkScheduled(); this.overdueGracePeriod = Objects.requireNonNull(period); - return this; + return self(); } @Override - public JobDefinition setTimeZone(String timeZone) { + public THIS setTimeZone(String timeZone) { checkScheduled(); this.timeZone = Objects.requireNonNull(timeZone); - return this; + return self(); } @Override - public JobDefinition setExecuteWith(String implementation) { + public THIS setExecuteWith(String implementation) { checkScheduled(); this.implementation = Objects.requireNonNull(implementation); - return this; + return self(); } @Override - public JobDefinition setExecutionMaxDelay(String maxDelay) { + public THIS setExecutionMaxDelay(String maxDelay) { checkScheduled(); this.executionMaxDelay = maxDelay; - return this; + return self(); } @Override - public JobDefinition setTask(Consumer task, boolean runOnVirtualThread) { + public THIS setTask(Consumer task, boolean runOnVirtualThread) { checkScheduled(); if (asyncTask != null) { throw new IllegalStateException("Async task was already set"); } this.task = Objects.requireNonNull(task); this.runOnVirtualThread = runOnVirtualThread; - return this; + return self(); } @Override - public JobDefinition setTask(Class> taskClass, boolean runOnVirtualThread) { + public THIS setTask(Class> taskClass, boolean runOnVirtualThread) { this.taskClass = Objects.requireNonNull(taskClass); return setTask(SchedulerUtils.instantiateBeanOrClass(taskClass), runOnVirtualThread); } @Override - public JobDefinition setAsyncTask(Function> asyncTask) { + public THIS setAsyncTask(Function> asyncTask) { checkScheduled(); if (task != null) { throw new IllegalStateException("Sync task was already set"); } this.asyncTask = Objects.requireNonNull(asyncTask); - return this; + return self(); } @Override - public JobDefinition setAsyncTask(Class>> asyncTaskClass) { + public THIS setAsyncTask(Class>> asyncTaskClass) { this.asyncTaskClass = Objects.requireNonNull(asyncTaskClass); return setAsyncTask(SchedulerUtils.instantiateBeanOrClass(asyncTaskClass)); } @@ -145,4 +145,9 @@ protected void checkScheduled() { } } + @SuppressWarnings("unchecked") + protected THIS self() { + return (THIS) this; + } + } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/programmatic/ProgrammaticJobsTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/programmatic/ProgrammaticJobsTest.java index 3bd4446a7a44f0..cd8910628bd538 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/programmatic/ProgrammaticJobsTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/programmatic/ProgrammaticJobsTest.java @@ -63,7 +63,7 @@ public void testJobs() throws InterruptedException { .setSkipPredicate(AlwaysSkipPredicate.class) .schedule(); - Scheduler.JobDefinition job1 = scheduler.newJob("foo") + Scheduler.JobDefinition job1 = scheduler.newJob("foo") .setInterval("1s") .setTask(ec -> { assertTrue(Arc.container().requestContext().isActive()); @@ -73,7 +73,7 @@ public void testJobs() throws InterruptedException { assertEquals("Sync task was already set", assertThrows(IllegalStateException.class, () -> job1.setAsyncTask(ec -> null)).getMessage()); - Scheduler.JobDefinition job2 = scheduler.newJob("foo").setCron("0/5 * * * * ?"); + Scheduler.JobDefinition job2 = scheduler.newJob("foo").setCron("0/5 * * * * ?"); assertEquals("Either sync or async task must be set", assertThrows(IllegalStateException.class, () -> job2.schedule()).getMessage()); job2.setTask(ec -> { @@ -110,7 +110,7 @@ public void testJobs() throws InterruptedException { @Test public void testAsyncJob() throws InterruptedException { - JobDefinition asyncJob = scheduler.newJob("fooAsync") + JobDefinition asyncJob = scheduler.newJob("fooAsync") .setInterval("1s") .setAsyncTask(ec -> { assertTrue(Context.isOnEventLoopThread() && VertxContext.isOnDuplicatedContext()); diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java index 3832f3a1fb4364..c41f17a47f9f0e 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java @@ -113,7 +113,7 @@ public Trigger getScheduledJob(String identity) { } @Override - public JobDefinition newJob(String identity) { + public CompositeJobDefinition newJob(String identity) { return new CompositeJobDefinition(identity); } @@ -133,14 +133,14 @@ public String implementation() { return Scheduled.AUTO; } - class CompositeJobDefinition extends AbstractJobDefinition { + public class CompositeJobDefinition extends AbstractJobDefinition { public CompositeJobDefinition(String identity) { super(identity); } @Override - public JobDefinition setExecuteWith(String implementation) { + public CompositeJobDefinition setExecuteWith(String implementation) { Objects.requireNonNull(implementation); if (!Scheduled.AUTO.equals(implementation)) { if (schedulers.stream().map(Scheduler::implementation).noneMatch(implementation::equals)) { @@ -164,7 +164,7 @@ public Trigger schedule() { throw new IllegalStateException("Matching scheduler implementation not found: " + implementation); } - private JobDefinition copy(JobDefinition to) { + private JobDefinition copy(JobDefinition to) { to.setCron(cron); to.setInterval(every); to.setDelayed(delayed); diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index 595a23e9404aa3..50950d516e0065 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -174,7 +174,7 @@ public String implementation() { } @Override - public JobDefinition newJob(String identity) { + public SimpleJobDefinition newJob(String identity) { if (!isStarted()) { throw notStarted(); } @@ -603,7 +603,7 @@ public Instant getScheduledFireTime() { } - class SimpleJobDefinition extends AbstractJobDefinition { + public class SimpleJobDefinition extends AbstractJobDefinition { private final SchedulerConfig schedulerConfig; diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 2345f482c5f805..3fc9752fd8f91d 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -560,7 +560,7 @@ private OASFilter getOperationFilter(OpenApiFilteredIndexViewBuildItem indexView if (!classNamesMethods.isEmpty() || !rolesAllowedMethods.isEmpty() || !authenticatedMethods.isEmpty()) { return new OperationFilter(classNamesMethods, rolesAllowedMethods, authenticatedMethods, config.securitySchemeName, - config.autoAddTags, config.autoAddOperationSummary); + config.autoAddTags, config.autoAddOperationSummary, isOpenApi_3_1_0_OrGreater(config)); } return null; @@ -1169,4 +1169,9 @@ private List getResourceFiles(Path resourcePath, Path target) { } return filenames; } + + private static boolean isOpenApi_3_1_0_OrGreater(SmallRyeOpenApiConfig config) { + final String openApiVersion = config.openApiVersion.orElse(null); + return openApiVersion == null || (!openApiVersion.startsWith("2") && !openApiVersion.startsWith("3.0")); + } } diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java index f33c0e56460bd1..640a919febf5da 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java @@ -42,12 +42,13 @@ public class OperationFilter implements OASFilter { private final String defaultSecuritySchemeName; private final boolean doAutoTag; private final boolean doAutoOperation; + private final boolean alwaysIncludeScopesValidForScheme; public OperationFilter(Map classNameMap, Map> rolesAllowedMethodReferences, List authenticatedMethodReferences, String defaultSecuritySchemeName, - boolean doAutoTag, boolean doAutoOperation) { + boolean doAutoTag, boolean doAutoOperation, boolean alwaysIncludeScopesValidForScheme) { this.classNameMap = Objects.requireNonNull(classNameMap); this.rolesAllowedMethodReferences = Objects.requireNonNull(rolesAllowedMethodReferences); @@ -55,13 +56,14 @@ public OperationFilter(Map classNameMap, this.defaultSecuritySchemeName = Objects.requireNonNull(defaultSecuritySchemeName); this.doAutoTag = doAutoTag; this.doAutoOperation = doAutoOperation; + this.alwaysIncludeScopesValidForScheme = alwaysIncludeScopesValidForScheme; } @Override public void filterOpenAPI(OpenAPI openAPI) { var securityScheme = getSecurityScheme(openAPI); String schemeName = securityScheme.map(Map.Entry::getKey).orElse(defaultSecuritySchemeName); - boolean scopesValidForScheme = securityScheme.map(Map.Entry::getValue) + boolean scopesValidForScheme = alwaysIncludeScopesValidForScheme || securityScheme.map(Map.Entry::getValue) .map(SecurityScheme::getType) .map(Set.of(SecurityScheme.Type.OAUTH2, SecurityScheme.Type.OPENIDCONNECT)::contains) .orElse(false); diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java index 7a95fe1aaf5f32..ecbd54b7f603b8 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java @@ -2,6 +2,7 @@ import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; @@ -65,20 +66,20 @@ void testAutoSecurityRequirement() { not(hasKey("my-extension2")))) .and() // OpenApiResourceSecuredAtMethodLevel - .body("paths.'/resource2/test-security/naked'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/naked'.get.security", defaultSecurityScheme("admin")) .body("paths.'/resource2/test-security/annotated'.get.security", defaultSecurity) - .body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurity) - .body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurityScheme("user1")) + .body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurityScheme("user2")) .body("paths.'/resource2/test-security/methodLevel/public'.get.security", nullValue()) .body("paths.'/resource2/test-security/annotated/documented'.get.security", defaultSecurity) - .body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurityScheme("admin")) .body("paths.'/resource2/test-security/methodLevel/4'.get.security", defaultSecurity) .and() // OpenApiResourceSecuredAtClassLevel - .body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurity) - .body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurityScheme("user1")) + .body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurityScheme("user2")) .body("paths.'/resource2/test-security/classLevel/3'.get.security", schemeArray("MyOwnName")) - .body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurityScheme("admin")) .and() // OpenApiResourceSecuredAtMethodLevel2 .body("paths.'/resource3/test-security/annotated'.get.security", schemeArray("AtClassLevel")) @@ -173,4 +174,11 @@ void testOpenAPIAnnotations() { Matchers.equalTo("Not Allowed")); } + static Matcher> defaultSecurityScheme(String... roles) { + return allOf( + iterableWithSize(1), + hasItem(allOf( + aMapWithSize(1), + hasEntry(equalTo("JWTCompanyAuthentication"), containsInAnyOrder(roles))))); + } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java new file mode 100644 index 00000000000000..fd7f6b9244352a --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java @@ -0,0 +1,183 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +/** + * Run same tests as {@link AutoSecurityRolesAllowedTestCase}, but with OpenAPI version 3.0.2 + * that only allowed security requirement scopes for Oauth2 and OpenID Connect schemes. + */ +public class AutoSecurityRolesAllowedUnsupportedScopesTestCase { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ResourceBean.class, OpenApiResourceSecuredAtClassLevel.class, + OpenApiResourceSecuredAtClassLevel2.class, OpenApiResourceSecuredAtMethodLevel.class, + OpenApiResourceSecuredAtMethodLevel2.class) + .addAsResource( + new StringAsset(""" + quarkus.smallrye-openapi.open-api-version=3.0.2 + quarkus.smallrye-openapi.security-scheme=jwt + quarkus.smallrye-openapi.security-scheme-name=JWTCompanyAuthentication + quarkus.smallrye-openapi.security-scheme-description=JWT Authentication + quarkus.smallrye-openapi.security-scheme-extensions.x-my-extension1=extension-value + quarkus.smallrye-openapi.security-scheme-extensions.my-extension2=extension-value + """), + "application.properties")); + + static Matcher> schemeArray(String schemeName) { + return allOf( + iterableWithSize(1), + hasItem(allOf( + aMapWithSize(1), + hasEntry(equalTo(schemeName), emptyIterable())))); + } + + @Test + void testAutoSecurityRequirement() { + var defaultSecurity = schemeArray("JWTCompanyAuthentication"); + + RestAssured.given() + .header("Accept", "application/json") + .when() + .get("/q/openapi") + .then() + .log().body() + .and() + .body("openapi", Matchers.is("3.0.2")) + .body("components.securitySchemes.JWTCompanyAuthentication", allOf( + hasEntry("type", "http"), + hasEntry("scheme", "bearer"), + hasEntry("bearerFormat", "JWT"), + hasEntry("description", "JWT Authentication"), + hasEntry("x-my-extension1", "extension-value"), + not(hasKey("my-extension2")))) + .and() + // OpenApiResourceSecuredAtMethodLevel + .body("paths.'/resource2/test-security/naked'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/annotated'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/public'.get.security", nullValue()) + .body("paths.'/resource2/test-security/annotated/documented'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/4'.get.security", defaultSecurity) + .and() + // OpenApiResourceSecuredAtClassLevel + .body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/classLevel/3'.get.security", schemeArray("MyOwnName")) + .body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurity) + .and() + // OpenApiResourceSecuredAtMethodLevel2 + .body("paths.'/resource3/test-security/annotated'.get.security", schemeArray("AtClassLevel")) + .and() + // OpenApiResourceSecuredAtClassLevel2 + .body("paths.'/resource3/test-security/classLevel-2/1'.get.security", defaultSecurity); + } + + @Test + void testOpenAPIAnnotations() { + RestAssured.given().header("Accept", "application/json") + .when().get("/q/openapi") + .then() + .log().body() + .and() + .body("paths.'/resource2/test-security/classLevel/1'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/classLevel/1'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/classLevel/2'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/classLevel/2'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/classLevel/3'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/3'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/4'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/classLevel/4'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")) + .and() + .body("paths.'/resource2/test-security/naked'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/naked'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/annotated'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/annotated'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/methodLevel/1'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/methodLevel/1'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/methodLevel/2'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/methodLevel/2'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/methodLevel/public'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/methodLevel/public'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/annotated/documented'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/annotated/documented'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")) + .and() + .body("paths.'/resource2/test-security/methodLevel/3'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/methodLevel/3'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")) + .and() + .body("paths.'/resource2/test-security/methodLevel/4'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/methodLevel/4'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource3/test-security/classLevel-2/1'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource3/test-security/classLevel-2/1'.get.responses.403.description", + Matchers.equalTo("Not Allowed")); + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java index 086456e1f0757a..c2063d53f6c88f 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java @@ -2,7 +2,7 @@ import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalToObject; import static org.hamcrest.Matchers.hasEntry; @@ -24,18 +24,18 @@ class AutoSecurityRolesAllowedWithInterfaceTestCase { .addClasses(ApplicationContext.class, FooAPI.class, FooResource.class)); - static Matcher> schemeArray(String schemeName) { + static Matcher> schemeArray(String schemeName, String... roles) { return allOf( iterableWithSize(1), hasItem(allOf( aMapWithSize(1), - hasEntry(equalTo(schemeName), emptyIterable())))); + hasEntry(equalTo(schemeName), containsInAnyOrder(roles))))); } @Test void testAutoSecurityRequirement() { - var oidcAuth = schemeArray("oidc_auth"); + var oidcAuth = schemeArray("oidc_auth", "RoleXY"); RestAssured.given() .header("Accept", "application/json") diff --git a/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java b/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java index ab09839b74d159..41acd5fe59f519 100644 --- a/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java +++ b/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java @@ -31,6 +31,7 @@ import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -286,6 +287,12 @@ ContextHandlerBuildItem createVertxContextHandlers(VertxCoreRecorder recorder, V return new ContextHandlerBuildItem(recorder.executionContextHandler(buildConfig.customizeArcContext())); } + @BuildStep(onlyIf = IsDevelopment.class) + @Record(ExecutionTime.RUNTIME_INIT) + public void resetMapper(VertxCoreRecorder recorder, ShutdownContextBuildItem shutdown) { + recorder.resetMapper(shutdown); + } + private void handleBlockingWarningsInDevOrTestMode() { try { Filter debuggerFilter = createDebuggerFilter(); diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java index d678e2dfa3ab0b..15ab4cd34fc982 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java @@ -49,6 +49,7 @@ import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.quarkus.vertx.mdc.provider.LateBoundMDCProvider; import io.quarkus.vertx.runtime.VertxCurrentContextFactory; +import io.quarkus.vertx.runtime.jackson.QuarkusJacksonFactory; import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Handler; @@ -583,6 +584,15 @@ public Thread newThread(Runnable runnable) { }; } + public void resetMapper(ShutdownContext shutdown) { + shutdown.addShutdownTask(new Runnable() { + @Override + public void run() { + QuarkusJacksonFactory.reset(); + } + }); + } + private static void setNewThreadTccl(VertxThread thread) { ClassLoader cl = VertxCoreRecorder.currentDevModeNewThreadCreationClassLoader; if (cl == null) { diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java index 665cc6ca0697e9..8847fcfb59f303 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java @@ -1,5 +1,7 @@ package io.quarkus.vertx.runtime.jackson; +import java.util.concurrent.atomic.AtomicInteger; + import io.vertx.core.json.jackson.DatabindCodec; import io.vertx.core.json.jackson.JacksonCodec; import io.vertx.core.spi.JsonFactory; @@ -10,6 +12,8 @@ */ public class QuarkusJacksonFactory implements JsonFactory { + private static final AtomicInteger COUNTER = new AtomicInteger(); + @Override public JsonCodec codec() { JsonCodec codec; @@ -25,7 +29,15 @@ public JsonCodec codec() { codec = new JacksonCodec(); } } + COUNTER.incrementAndGet(); return codec; } + public static void reset() { + // if we blindly reset, we could get NCDFE because Jackson classes would not have been loaded + if (COUNTER.get() > 0) { + QuarkusJacksonJsonCodec.reset(); + } + } + } diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonJsonCodec.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonJsonCodec.java index 995812897da5b8..1e02b5e8fef81e 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonJsonCodec.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonJsonCodec.java @@ -30,11 +30,31 @@ */ class QuarkusJacksonJsonCodec implements JsonCodec { - private static final ObjectMapper mapper; + private static volatile ObjectMapper mapper; // we don't want to create this unless it's absolutely necessary (and it rarely is) private static volatile ObjectMapper prettyMapper; static { + populateMapper(); + } + + public static void reset() { + mapper = null; + prettyMapper = null; + } + + private static ObjectMapper mapper() { + if (mapper == null) { + synchronized (QuarkusJacksonJsonCodec.class) { + if (mapper == null) { + populateMapper(); + } + } + } + return mapper; + } + + private static void populateMapper() { ArcContainer container = Arc.container(); if (container == null) { // this can happen in QuarkusUnitTest @@ -74,7 +94,7 @@ class QuarkusJacksonJsonCodec implements JsonCodec { private ObjectMapper prettyMapper() { if (prettyMapper == null) { - prettyMapper = mapper.copy(); + prettyMapper = mapper().copy(); prettyMapper.configure(SerializationFeature.INDENT_OUTPUT, true); } return prettyMapper; @@ -83,7 +103,7 @@ private ObjectMapper prettyMapper() { @SuppressWarnings("unchecked") @Override public T fromValue(Object json, Class clazz) { - T value = QuarkusJacksonJsonCodec.mapper.convertValue(json, clazz); + T value = QuarkusJacksonJsonCodec.mapper().convertValue(json, clazz); if (clazz == Object.class) { value = (T) adapt(value); } @@ -102,7 +122,7 @@ public T fromBuffer(Buffer buf, Class clazz) throws DecodeException { public static JsonParser createParser(Buffer buf) { try { - return QuarkusJacksonJsonCodec.mapper.getFactory() + return QuarkusJacksonJsonCodec.mapper().getFactory() .createParser((InputStream) new ByteBufInputStream(buf.getByteBuf())); } catch (IOException e) { throw new DecodeException("Failed to decode:" + e.getMessage(), e); @@ -111,7 +131,7 @@ public static JsonParser createParser(Buffer buf) { public static JsonParser createParser(String str) { try { - return QuarkusJacksonJsonCodec.mapper.getFactory().createParser(str); + return QuarkusJacksonJsonCodec.mapper().getFactory().createParser(str); } catch (IOException e) { throw new DecodeException("Failed to decode:" + e.getMessage(), e); } @@ -122,7 +142,7 @@ public static T fromParser(JsonParser parser, Class type) throws DecodeEx T value; JsonToken remaining; try { - value = QuarkusJacksonJsonCodec.mapper.readValue(parser, type); + value = QuarkusJacksonJsonCodec.mapper().readValue(parser, type); remaining = parser.nextToken(); } catch (Exception e) { throw new DecodeException("Failed to decode:" + e.getMessage(), e); @@ -141,7 +161,7 @@ public static T fromParser(JsonParser parser, Class type) throws DecodeEx @Override public String toString(Object object, boolean pretty) throws EncodeException { try { - ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper; + ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper(); return mapper.writeValueAsString(object); } catch (Exception e) { throw new EncodeException("Failed to encode as JSON: " + e.getMessage(), e); @@ -151,7 +171,7 @@ public String toString(Object object, boolean pretty) throws EncodeException { @Override public Buffer toBuffer(Object object, boolean pretty) throws EncodeException { try { - ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper; + ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper(); return Buffer.buffer(mapper.writeValueAsBytes(object)); } catch (Exception e) { throw new EncodeException("Failed to encode as JSON: " + e.getMessage(), e); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/jbang/JBangDevModeLauncherImpl.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/jbang/JBangDevModeLauncherImpl.java index 207281a09cb358..49dbca6aee629e 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/jbang/JBangDevModeLauncherImpl.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/jbang/JBangDevModeLauncherImpl.java @@ -15,6 +15,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Map; +import java.util.Properties; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -85,7 +86,7 @@ public static Closeable main(String... args) { Path srcDir = projectRoot.resolve("src/main/java"); Files.createDirectories(srcDir); - Files.createSymbolicLink(srcDir.resolve(sourceFile.getFileName().toString()), sourceFile); + Path source = Files.createSymbolicLink(srcDir.resolve(sourceFile.getFileName().toString()), sourceFile); final LocalProject currentProject = LocalProject.loadWorkspace(projectRoot); final ResolvedDependency appArtifact = ResolvedDependencyBuilder.newInstance() .setCoords(currentProject.getAppArtifact(ArtifactCoords.TYPE_JAR)) @@ -93,6 +94,8 @@ public static Closeable main(String... args) { .setWorkspaceModule(currentProject.toWorkspaceModule()) .build(); + Properties configurationProperties = getConfigurationProperties(source); + //todo : proper support for everything final QuarkusBootstrap.Builder builder = QuarkusBootstrap.builder() .setBaseClassLoader(JBangDevModeLauncherImpl.class.getClassLoader()) @@ -117,7 +120,9 @@ public static Closeable main(String... args) { return artifact; }).collect(Collectors.toList())) .setApplicationRoot(targetClasses) - .setProjectRoot(projectRoot); + .setProjectRoot(projectRoot) + .setBuildSystemProperties(configurationProperties) + .setRuntimeProperties(configurationProperties); Map context = new HashMap<>(); context.put("app-project", currentProject); @@ -174,4 +179,19 @@ private static String getQuarkusVersion() { throw new RuntimeException(e); } } + + private static Properties getConfigurationProperties(final Path source) throws IOException { + Properties properties = new Properties(); + for (String line : Files.readAllLines(source)) { + if (line.startsWith("//Q:CONFIG")) { + String conf = line.substring(10).trim(); + int equals = conf.indexOf("="); + if (equals == -1) { + throw new RuntimeException("invalid config " + line); + } + properties.setProperty(conf.substring(0, equals), conf.substring(equals + 1)); + } + } + return properties; + } } diff --git a/independent-projects/enforcer-rules/pom.xml b/independent-projects/enforcer-rules/pom.xml index a1a87c7a5af248..488f1e42a002ca 100644 --- a/independent-projects/enforcer-rules/pom.xml +++ b/independent-projects/enforcer-rules/pom.xml @@ -104,7 +104,7 @@ org.apache.groovy groovy - 4.0.23 + 4.0.24 diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithNodeSelectorTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithNodeSelectorTest.java new file mode 100644 index 00000000000000..950eb433c088d5 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithNodeSelectorTest.java @@ -0,0 +1,55 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithNodeSelectorTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName("nodeselector") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("kubernetes-with-nodeselector.properties"); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Map expectedNodeSelector = Map.of("diskType", "ssd"); + + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("nodeselector"); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getNodeSelector()).containsExactlyEntriesOf(expectedNodeSelector); + }); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-nodeselector.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-nodeselector.properties new file mode 100644 index 00000000000000..b14b91278ff67b --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-nodeselector.properties @@ -0,0 +1,2 @@ +quarkus.kubernetes.node-selector.key=diskType +quarkus.kubernetes.node-selector.value=ssd \ No newline at end of file diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcEventResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcEventResource.java index 7a888ee8217e0e..43292799e4805f 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcEventResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcEventResource.java @@ -16,7 +16,7 @@ public class OidcEventResource { private final String expectedAuthServerUrl; public OidcEventResource(OidcEventObserver oidcEventObserver, OidcConfig oidcConfig) { - this.expectedAuthServerUrl = dropTrailingSlash(oidcConfig.defaultTenant().authServerUrl().get()); + this.expectedAuthServerUrl = dropTrailingSlash(OidcConfig.getDefaultTenant(oidcConfig).authServerUrl().get()); this.oidcEventObserver = oidcEventObserver; } diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/SimpleProfile.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/SimpleProfile.java new file mode 100644 index 00000000000000..b0ea9d04a013e3 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/SimpleProfile.java @@ -0,0 +1,12 @@ +package io.quarkus.it.opentelemetry.vertx.exporter; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public final class SimpleProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.otel.simple", "true"); + } +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSNoCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSNoCompressionTest.java new file mode 100644 index 00000000000000..0c82fae42e1efc --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSNoCompressionTest.java @@ -0,0 +1,15 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleGrpcNoTLSNoCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSWithCompressionTest.java new file mode 100644 index 00000000000000..407fd02c01fa78 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSWithCompressionTest.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = @ResourceArg(name = "enableCompression", value = "true"), restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleGrpcNoTLSWithCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSNoCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSNoCompressionTest.java new file mode 100644 index 00000000000000..8c003fdacf7950 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSNoCompressionTest.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = @ResourceArg(name = "enableTLS", value = "true"), restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleGrpcWithTLSNoCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithCompressionTest.java new file mode 100644 index 00000000000000..b7645211468820 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithCompressionTest.java @@ -0,0 +1,19 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableTLS", value = "true"), + @ResourceArg(name = "enableCompression", value = "true") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleGrpcWithTLSWithCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithTrustAllWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithTrustAllWithCompressionTest.java new file mode 100644 index 00000000000000..45ce4303120f3d --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithTrustAllWithCompressionTest.java @@ -0,0 +1,32 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(SimpleGrpcWithTLSWithTrustAllWithCompressionTest.Profile.class) +public class SimpleGrpcWithTLSWithTrustAllWithCompressionTest extends AbstractExporterTest { + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.tls.trust-all", "true", "quarkus.otel.simple", "true"); + } + + @Override + public List testResources() { + return Collections.singletonList( + new TestResourceEntry( + OtelCollectorLifecycleManager.class, + Map.of("enableTLS", "true", "enableCompression", "true", "preventTrustCert", "true"))); + } + } + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSNoCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSNoCompressionTest.java new file mode 100644 index 00000000000000..f623ed071d58b6 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSNoCompressionTest.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = @ResourceArg(name = "protocol", value = "http/protobuf"), restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpNoTLSNoCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSWithCompressionTest.java new file mode 100644 index 00000000000000..8fbc960b5e9be5 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSWithCompressionTest.java @@ -0,0 +1,19 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableCompression", value = "true"), + @ResourceArg(name = "protocol", value = "http/protobuf") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpNoTLSWithCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSNoCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSNoCompressionTest.java new file mode 100644 index 00000000000000..a5fb15970a28c9 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSNoCompressionTest.java @@ -0,0 +1,19 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableTLS", value = "true"), + @ResourceArg(name = "protocol", value = "http/protobuf") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpWithTLSNoCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionTest.java new file mode 100644 index 00000000000000..77188c2cf61f5a --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionTest.java @@ -0,0 +1,20 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableTLS", value = "true"), + @ResourceArg(name = "enableCompression", value = "true"), + @ResourceArg(name = "protocol", value = "http/protobuf") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpWithTLSWithCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionUsingRegistryTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionUsingRegistryTest.java new file mode 100644 index 00000000000000..31a896345339f9 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionUsingRegistryTest.java @@ -0,0 +1,21 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableTLS", value = "true"), + @ResourceArg(name = "enableCompression", value = "true"), + @ResourceArg(name = "protocol", value = "http/protobuf"), + @ResourceArg(name = "tlsRegistryName", value = "otel") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpWithTLSWithCompressionUsingRegistryTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithTrustAllWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithTrustAllWithCompressionTest.java new file mode 100644 index 00000000000000..887ab7c133df40 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithTrustAllWithCompressionTest.java @@ -0,0 +1,33 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(SimpleHttpWithTLSWithTrustAllWithCompressionTest.Profile.class) +public class SimpleHttpWithTLSWithTrustAllWithCompressionTest extends AbstractExporterTest { + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.tls.trust-all", "true", "quarkus.otel.simple", "true"); + } + + @Override + public List testResources() { + return Collections.singletonList( + new TestResourceEntry( + OtelCollectorLifecycleManager.class, + Map.of("enableTLS", "true", "enableCompression", "true", "preventTrustCert", "true", "protocol", + "http/protobuf"))); + } + } + +} diff --git a/integration-tests/test-extension/extension/deployment/src/main/resources/application-socket-output.properties b/integration-tests/test-extension/extension/deployment/src/main/resources/application-socket-output.properties new file mode 100644 index 00000000000000..92b64a1e93a19e --- /dev/null +++ b/integration-tests/test-extension/extension/deployment/src/main/resources/application-socket-output.properties @@ -0,0 +1,8 @@ +quarkus.log.level=INFO +quarkus.log.socket.enable=true +quarkus.log.socket.endpoint=localhost:5140 +quarkus.log.socket.protocol=TCP +quarkus.log.socket.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.socket.level=WARNING +# Resource path to DSAPublicKey base64 encoded bytes +quarkus.root.dsa-key-location=/DSAPublicKey.encoded \ No newline at end of file diff --git a/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/logging/SocketHandlerTest.java b/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/logging/SocketHandlerTest.java new file mode 100644 index 00000000000000..754acfc58a4c59 --- /dev/null +++ b/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/logging/SocketHandlerTest.java @@ -0,0 +1,42 @@ +package io.quarkus.logging; + +import static io.quarkus.logging.LoggingTestsHelper.getHandler; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; + +import org.jboss.logmanager.formatters.PatternFormatter; +import org.jboss.logmanager.handlers.SocketHandler; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +class SocketHandlerTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-socket-output.properties") + .withApplicationRoot((jar) -> jar + .addClass(LoggingTestsHelper.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")); + + @Test + void socketOutputTest() { + Handler handler = getHandler(SocketHandler.class); + assertThat(handler.getLevel()).isEqualTo(Level.WARNING); + + Formatter formatter = handler.getFormatter(); + assertThat(formatter).isInstanceOf(PatternFormatter.class); + PatternFormatter patternFormatter = (PatternFormatter) formatter; + assertThat(patternFormatter.getPattern()).isEqualTo("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n"); + + SocketHandler socketHandler = (SocketHandler) handler; + assertThat(socketHandler.getPort()).isEqualTo(5140); + assertThat(socketHandler.getAddress().getHostAddress()).isEqualTo("127.0.0.1"); + assertThat(socketHandler.getProtocol()).isEqualTo(SocketHandler.Protocol.TCP); + assertThat(socketHandler.isBlockOnReconnect()).isFalse(); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9819825936fbe7..94c31a88da386d 100644 --- a/pom.xml +++ b/pom.xml @@ -183,7 +183,7 @@ io.quarkus.bot build-reporter-maven-extension - 3.9.5 + 3.9.6