From 3d110d1b0a1675414627cfcd723c21f1b7d9ad1e Mon Sep 17 00:00:00 2001 From: Antoine de Troostembergh Date: Sun, 3 Nov 2024 20:54:47 +0100 Subject: [PATCH 01/23] add socket log appender and basic ECS json formatting --- .../builditem/LogSocketFormatBuildItem.java | 36 +++++++++ .../logging/LoggingResourceProcessor.java | 5 ++ .../runtime/logging/LogRuntimeConfig.java | 71 +++++++++++++++++ .../runtime/logging/LoggingSetupRecorder.java | 76 ++++++++++++++++++- .../asciidoc/centralized-log-management.adoc | 72 ++++++++++++++++++ docs/src/main/asciidoc/logging.adoc | 18 +++++ .../json/deployment/LoggingJsonProcessor.java | 7 ++ .../SocketJsonFormatterCustomConfigTest.java | 72 ++++++++++++++++++ .../SocketJsonFormatterDefaultConfigTest.java | 62 +++++++++++++++ ...on-socket-json-formatter-custom.properties | 16 ++++ ...n-socket-json-formatter-default.properties | 5 ++ .../logging/json/runtime/JsonLogConfig.java | 18 +++++ .../json/runtime/LoggingJsonRecorder.java | 60 +++++++++++++++ .../application-socket-output.properties | 8 ++ .../io/quarkus/logging/SocketHandlerTest.java | 42 ++++++++++ 15 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/builditem/LogSocketFormatBuildItem.java create mode 100644 extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SocketJsonFormatterCustomConfigTest.java create mode 100644 extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SocketJsonFormatterDefaultConfigTest.java create mode 100644 extensions/logging-json/deployment/src/test/resources/application-socket-json-formatter-custom.properties create mode 100644 extensions/logging-json/deployment/src/test/resources/application-socket-json-formatter-default.properties create mode 100644 integration-tests/test-extension/extension/deployment/src/main/resources/application-socket-output.properties create mode 100644 integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/logging/SocketHandlerTest.java 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 0000000000000..23aeaf109b955 --- /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 a94f8fdc3894f..8507399dea96c 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/runtime/src/main/java/io/quarkus/runtime/logging/LogRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogRuntimeConfig.java index ed01e44255ffa..09cb613d2ff21 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. */ @@ -393,6 +411,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 6cea3eb817823..2d390ab877db0 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/docs/src/main/asciidoc/centralized-log-management.adoc b/docs/src/main/asciidoc/centralized-log-management.adoc index 663df993637c1..4fbb13b06388c 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/logging.adoc b/docs/src/main/asciidoc/logging.adoc index aa47489dade9a..8d1491466bbde 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/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 47ef0ba81a332..44db47e3c9b7f 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 0000000000000..e6e0d8a9ac0a2 --- /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 0000000000000..99d25bfa99805 --- /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 0000000000000..0441faac79159 --- /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 0000000000000..31933e9601d37 --- /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 35ec19204e441..3f1cd1fffd9d4 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 872f292569dad..d05745e2a76ca 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/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 0000000000000..92b64a1e93a19 --- /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 0000000000000..754acfc58a4c5 --- /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 From 2ffe01295d7fea5814f03657a29edeca770599ca Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 31 Oct 2024 13:56:39 +0100 Subject: [PATCH 02/23] Quartz: introduce Nonconcurrent - the behavior is identical to a Job class annotated with DisallowConcurrentExecution - fixes #44048 --- .../quartz/deployment/QuartzProcessor.java | 21 +++- .../test/NonconcurrentJobDefinitionTest.java | 67 ++++++++++++ .../test/NonconcurrentOnQuartzThreadTest.java | 56 ++++++++++ .../test/NonconcurrentProgrammaticTest.java | 91 ++++++++++++++++ .../quartz/test/NonconcurrentTest.java | 54 ++++++++++ .../programmatic/ProgrammaticJobsTest.java | 6 +- .../java/io/quarkus/quartz/Nonconcurrent.java | 35 ++++++ .../io/quarkus/quartz/QuartzScheduler.java | 14 +++ .../quartz/runtime/QuartzRecorder.java | 5 +- .../quartz/runtime/QuartzSchedulerImpl.java | 101 ++++++++++++++---- .../quarkus/quartz/runtime/QuartzSupport.java | 19 +++- .../java/io/quarkus/scheduler/Scheduler.java | 36 +++---- .../common/runtime/AbstractJobDefinition.java | 57 +++++----- .../programmatic/ProgrammaticJobsTest.java | 6 +- .../scheduler/runtime/CompositeScheduler.java | 8 +- .../scheduler/runtime/SimpleScheduler.java | 4 +- 16 files changed, 499 insertions(+), 81 deletions(-) create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentJobDefinitionTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentOnQuartzThreadTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentProgrammaticTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentTest.java create mode 100644 extensions/quartz/runtime/src/main/java/io/quarkus/quartz/Nonconcurrent.java 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 81e2739284704..9c6dfe0f27814 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 0000000000000..6cb0446db3528 --- /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 0000000000000..dee4f50683f48 --- /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 0000000000000..5ebeb934053c7 --- /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 0000000000000..ec26ba9be0524 --- /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 d2f5e62a5a55e..aa027694004b9 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 0000000000000..27bcaa2104b04 --- /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 395a6de8369a4..60c30ab3d7292 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 9a1bd26cae449..7ea820528fcd6 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 e67a8c9f41ac9..a231c29e65b61 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 b343422373b78..18944bba97041 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/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java index d0d2467c160cc..97dbebb4014c5 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 d94f1c612a378..d7a391628b7dd 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 3bd4446a7a44f..cd8910628bd53 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 3832f3a1fb436..c41f17a47f9f0 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 595a23e9404aa..50950d516e006 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; From dd2201e24278825c32649ad8b122e4299f44dbba Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Thu, 7 Nov 2024 15:32:47 -0600 Subject: [PATCH 03/23] Clarify logging rotation docs a little bit Fixes #44346 --- .../main/java/io/quarkus/runtime/logging/LogRuntimeConfig.java | 3 +++ 1 file changed, 3 insertions(+) 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 ed01e44255ffa..ce35cf5d9521e 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 @@ -207,6 +207,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) From b0339bdf2c74bca1ac801f1423c40efaaecd0ed9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:55:05 +0000 Subject: [PATCH 04/23] Bump flyway.version from 10.20.1 to 10.21.0 Bumps `flyway.version` from 10.20.1 to 10.21.0. Updates `org.flywaydb:flyway-core` from 10.20.1 to 10.21.0 - [Release notes](https://github.com/flyway/flyway/releases) - [Commits](https://github.com/flyway/flyway/compare/flyway-10.20.1...flyway-10.21.0) Updates `org.flywaydb:flyway-sqlserver` from 10.20.1 to 10.21.0 Updates `org.flywaydb:flyway-mysql` from 10.20.1 to 10.21.0 Updates `org.flywaydb:flyway-database-oracle` from 10.20.1 to 10.21.0 Updates `org.flywaydb:flyway-database-postgresql` from 10.20.1 to 10.21.0 Updates `org.flywaydb:flyway-database-db2` from 10.20.1 to 10.21.0 Updates `org.flywaydb:flyway-database-derby` from 10.20.1 to 10.21.0 Updates `org.flywaydb:flyway-database-mongodb` from 10.20.1 to 10.21.0 --- updated-dependencies: - dependency-name: org.flywaydb:flyway-core dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.flywaydb:flyway-sqlserver dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.flywaydb:flyway-mysql dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.flywaydb:flyway-database-oracle dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.flywaydb:flyway-database-postgresql dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.flywaydb:flyway-database-db2 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.flywaydb:flyway-database-derby dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.flywaydb:flyway-database-mongodb dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index b710b0b8ce0a0..8a582bec9e537 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 From 959a2e1e7de1c608ab9a76dfcbed7bdf85be7153 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 8 Nov 2024 08:43:36 +0100 Subject: [PATCH 05/23] Qute: fix generation of qute-i18n-examples - handle localized enums correctly - fixes #44366 --- .../MessageBundleMethodBuildItem.java | 17 ++++- .../deployment/MessageBundleProcessor.java | 25 ++++++-- .../MessageBundleEnumExampleFileTest.java | 64 +++++++++++++++++++ .../java/io/quarkus/qute/i18n/Message.java | 3 +- 4 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumExampleFileTest.java 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 56809719f7b0a..725999bbd0b9a 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 f67dd11dbc181..5842bbd715abc 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/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 0000000000000..008a289fa6340 --- /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 93c5fbe6b1327..b8b8a43ae5955 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}

From 77747da5a86d18ecb45827a43b3493bfe96285ec Mon Sep 17 00:00:00 2001
From: Georgios Andrianakis 
Date: Thu, 7 Nov 2024 20:22:31 +0200
Subject: [PATCH 06/23] Ensure that custom Jackson modules work in dev-mode

Fixes: #44231
---
 .../test/CustomModuleLiveReloadTest.java      | 144 ++++++++++++++++++
 .../core/deployment/VertxCoreProcessor.java   |   7 +
 .../vertx/core/runtime/VertxCoreRecorder.java |  10 ++
 .../jackson/QuarkusJacksonFactory.java        |  12 ++
 .../jackson/QuarkusJacksonJsonCodec.java      |  36 ++++-
 5 files changed, 201 insertions(+), 8 deletions(-)
 create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomModuleLiveReloadTest.java

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 0000000000000..b8db186126970
--- /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/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 ab09839b74d15..41acd5fe59f51 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 d678e2dfa3ab0..15ab4cd34fc98 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 665cc6ca0697e..8847fcfb59f30 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 995812897da5b..1e02b5e8fef81 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);

From bab4a5a351ef89436ae056b7b2e860dc9e650244 Mon Sep 17 00:00:00 2001
From: Guillaume Smet 
Date: Fri, 8 Nov 2024 14:46:20 +0100
Subject: [PATCH 07/23] Fix some invalid configuration cases

TransactionManagerConfiguration was just plain weird so fixed it.
For the others, we need to mark the interfaces as being config or we
won't generate the Javadoc for them.
---
 .../common/deployment/CommonConfig.java       |  2 +
 .../TransactionManagerConfiguration.java      | 84 +++++++++----------
 .../config/OidcClientCommonConfig.java        |  2 +
 .../runtime/config/OidcCommonConfig.java      |  2 +
 4 files changed, 48 insertions(+), 42 deletions(-)

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 426240ae842ed..e3ff5f25f4984 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/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 edba7b764aa0f..5f55cb5b6f8f4 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 8ad80d388a927..31806c933ce7a 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,10 @@ import java.util.Optional; import io.quarkus.runtime.annotations.ConfigDocMapKey; +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; 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 4edf4be8a9d46..211b825ecf325 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,10 @@ import java.util.OptionalInt; import io.quarkus.runtime.annotations.ConfigDocDefault; +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`. From 1813c5a9c775310712033af6fa2448c73dde63f2 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 8 Nov 2024 14:47:20 +0100 Subject: [PATCH 08/23] Generate a report for missing Javadoc and fail the build if not empty --- .../config/doc/GenerateConfigDocMojo.java | 20 +++++++++- .../doc/generator/AbstractFormatter.java | 25 ++++++++---- .../doc/generator/AsciidocFormatter.java | 4 +- .../maven/config/doc/generator/Formatter.java | 7 ++-- .../doc/generator/GenerationReport.java | 39 +++++++++++++++++++ .../doc/generator/MarkdownFormatter.java | 4 +- 6 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/GenerationReport.java 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 7b68c1ee47259..ba0cf3fff9f25 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 c2aca8ffb55e0..340e2c12f3c02 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 007a06c89efc0..75d8b1a370ecf 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 de25d1eebdb1e..742c571c2b2e6 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 0000000000000..bf3894389d95d --- /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 d5096c259317a..cc23c399d0352 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 From f983e20109458bd556c18f6398660056b7c3b4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Fri, 8 Nov 2024 16:06:21 +0100 Subject: [PATCH 09/23] Start Keycloak Dev Svc for standalone KC Admin client --- .../KeycloakDevServicesConfigurator.java | 3 +- .../KeycloakDevServicesProcessor.java | 28 ++++++---- .../common/KeycloakAdminClientConfig.java | 7 +-- .../deployment/pom.xml | 9 ++-- .../KeycloakDevServiceRequiredBuildStep.java | 32 +++++++++++ ...akAdminClientInjectionDevServicesTest.java | 12 +++-- ...akAdminClientMutualTlsDevServicesTest.java | 12 +++-- ...kAdminClientZeroConfigDevServicesTest.java | 53 +++++++++++++++++++ .../resources/app-dev-mode-config.properties | 1 - .../deployment/pom.xml | 9 ++-- .../KeycloakDevServiceRequiredBuildStep.java | 32 +++++++++++ ...akAdminClientInjectionDevServicesTest.java | 12 +++-- ...akAdminClientMutualTlsDevServicesTest.java | 12 +++-- ...kAdminClientZeroConfigDevServicesTest.java | 53 +++++++++++++++++++ .../resources/app-dev-mode-config.properties | 1 - 15 files changed, 239 insertions(+), 37 deletions(-) create mode 100644 extensions/keycloak-admin-rest-client/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/devservices/KeycloakDevServiceRequiredBuildStep.java create mode 100644 extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientZeroConfigDevServicesTest.java create mode 100644 extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/devservices/KeycloakDevServiceRequiredBuildStep.java create mode 100644 extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientZeroConfigDevServicesTest.java 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 a06f5df1a14b9..eff7e67bea529 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 f366769ad048b..6663e45f3e8b4 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 004200936b2bd..aab53d23fda50 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 8ff302c1ac65f..32d5c33b5db81 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 0000000000000..1d798dda51593 --- /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 b2c20096410c6..1551f3dfb1577 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 01542b0770234..753eabfe19bc3 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 0000000000000..fe3853598015a --- /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 47e6f9633d8e6..ab1a9dac13289 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 882b34d8c8230..598684441f300 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 0000000000000..1d330fa8b9ded --- /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 023213d4b211b..a87d149b61ca5 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 b628adb65aed5..33eaac20f98ad 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 0000000000000..440ed7514621d --- /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 47e6f9633d8e6..ab1a9dac13289 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} From a4d7e2fea50d370e5517e8ae2581200b0a593e05 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 8 Nov 2024 15:46:23 +0100 Subject: [PATCH 10/23] Fix runtime/deployment detection and propagate it That way, we can assert if the module is a deployment module later in the process. Related to https://github.com/quarkusio/quarkus/pull/43513 --- .../ExtensionAnnotationProcessor.java | 11 ++-- .../config/model/ExtensionModule.java | 20 ++++++ .../annotation/processor/util/Config.java | 13 ++-- .../processor/util/ExtensionUtil.java | 63 ++++++++++++------- 4 files changed, 74 insertions(+), 33 deletions(-) create mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ExtensionModule.java 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 c224ba30c764b..646d0e5cc345d 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 0000000000000..09f0b3150af6b --- /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 85643f62c24eb..8440155cca060 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 f1d6f434c8855..81b203a1085f8 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; } } From 3f10b51a200b3f9bede1c1e20baadc9144d5fb27 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Thu, 7 Nov 2024 19:12:09 +0000 Subject: [PATCH 11/23] Improve the way OIDC tenants are grouped and their properties are generated --- .../DefaultPolicyEnforcerResolver.java | 2 +- .../runtime/KeycloakPolicyEnforcerUtil.java | 2 +- .../config/OidcClientCommonConfig.java | 7 +++- .../runtime/config/OidcCommonConfig.java | 7 ++-- .../oidc/deployment/OidcBuildTimeConfig.java | 4 ++- .../quarkus/oidc/test/ProtectedResource.java | 2 +- .../ProtectedResourceWithJwtAccessToken.java | 2 +- ...rotectedResourceWithoutJwtAccessToken.java | 2 +- .../runtime/BackChannelLogoutHandler.java | 2 +- .../io/quarkus/oidc/runtime/OidcConfig.java | 27 ++++++++++------ .../io/quarkus/oidc/runtime/OidcRecorder.java | 7 ++-- .../oidc/runtime/OidcTenantConfig.java | 32 +++++++++++++------ .../it/keycloak/OidcEventResource.java | 2 +- 13 files changed, 67 insertions(+), 31 deletions(-) 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 f7ec6d61f7215..92c560fc6946c 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 47330c2f01caa..4ee31b741933f 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/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 31806c933ce7a..07318e9f4c18a 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,6 +4,7 @@ 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; @@ -36,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 211b825ecf325..1af74365734fd 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,6 +6,7 @@ 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; @@ -77,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/OidcBuildTimeConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java index 58cc8d51e3722..2060330618e6c 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 b4dd761333862..c3fd1eb15538b 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 59d17dd128ab7..a1adaa128fcce 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 3305810d605be..8e624842499a7 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 020a3ba2dc80a..e1679ef7b851a 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 a92a8ff0f67bf..c0632cb11c887 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 9dd8de738e6ee..086148a1eb96f 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 bd34006639614..f3c9c1463458c 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/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 7a888ee8217e0..43292799e4805 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; } From 48d8fb7155d7d4825397a8954e3ba8e23326f924 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:24:14 +0000 Subject: [PATCH 12/23] Bump io.quarkus.bot:build-reporter-maven-extension from 3.9.5 to 3.9.6 Bumps [io.quarkus.bot:build-reporter-maven-extension](https://github.com/quarkusio/build-reporter) from 3.9.5 to 3.9.6. - [Commits](https://github.com/quarkusio/build-reporter/compare/3.9.5...3.9.6) --- updated-dependencies: - dependency-name: io.quarkus.bot:build-reporter-maven-extension dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9819825936fbe..94c31a88da386 100644 --- a/pom.xml +++ b/pom.xml @@ -183,7 +183,7 @@ io.quarkus.bot build-reporter-maven-extension - 3.9.5 + 3.9.6 From 9361944617222e463af09278eb313b73da566423 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:27:26 +0000 Subject: [PATCH 13/23] Bump org.apache.groovy:groovy from 4.0.23 to 4.0.24 Bumps [org.apache.groovy:groovy](https://github.com/apache/groovy) from 4.0.23 to 4.0.24. - [Commits](https://github.com/apache/groovy/commits) --- updated-dependencies: - dependency-name: org.apache.groovy:groovy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build-parent/pom.xml | 2 +- independent-projects/enforcer-rules/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 21755e0b2979f..bd780c85a57ff 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/independent-projects/enforcer-rules/pom.xml b/independent-projects/enforcer-rules/pom.xml index a1a87c7a5af24..488f1e42a002c 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 From af78f41f6cc00817a3f6c0e605095aa7c5d5677c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:28:30 +0000 Subject: [PATCH 14/23] Bump com.nimbusds:nimbus-jose-jwt from 9.45 to 9.46 Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.45 to 9.46. - [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt) - [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.46..9.45) --- updated-dependencies: - dependency-name: com.nimbusds:nimbus-jose-jwt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 8a582bec9e537..aa01958f1765b 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -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 From bfb59fce578411d927959578f75aff2e0b2873d1 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Fri, 8 Nov 2024 23:18:02 +0000 Subject: [PATCH 15/23] Fix Keycloak DevService property doc typo --- .../quarkus/devservices/keycloak/KeycloakDevServicesConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5652b15cc1ff9..77bdc8c63d43b 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. From e5a21e4a486f5e0076e0e8a831e300e8096b8507 Mon Sep 17 00:00:00 2001 From: Vincent Sourin Date: Sun, 27 Oct 2024 13:31:27 +0100 Subject: [PATCH 16/23] Add nodeSelect capability to kubernetes extension. Fix https://github.com/quarkusio/quarkus/issues/44122 Signed-off-by: Vinche --- .../asciidoc/deploying-to-kubernetes.adoc | 21 +++++++ .../deployment/AddNodeSelectorDecorator.java | 24 ++++++++ .../deployment/KnativeProcessor.java | 2 + .../deployment/KubernetesCommonHelper.java | 4 ++ .../deployment/NodeSelectorConfig.java | 13 +++++ .../deployment/PlatformConfiguration.java | 5 ++ .../deployment/KubernetesConfigTest.java | 4 ++ .../application-kubernetes.properties | 3 + .../KubernetesWithNodeSelectorTest.java | 55 +++++++++++++++++++ .../kubernetes-with-nodeselector.properties | 2 + 10 files changed, 133 insertions(+) create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNodeSelectorDecorator.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/NodeSelectorConfig.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithNodeSelectorTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-nodeselector.properties diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 40f358744db22..8ab61ce549e68 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/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 0000000000000..c646ac9525ef9 --- /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 03aa9446595b1..a19726c00c5da 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 68160092bc238..6c1f43f2e3d21 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 0000000000000..681e7de03043e --- /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 11c78c45cc24f..51a8e40ab7fc3 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 4e75fe48d3e1f..1ddc093509987 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 467f9ad9fcb82..bdf4d8a914544 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/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 0000000000000..950eb433c088d --- /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 0000000000000..b14b91278ff67 --- /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 From 998c3ee2cdb0e6a01012d9b4e4d931a7f3574058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sun, 10 Nov 2024 10:39:10 +0100 Subject: [PATCH 17/23] Handle InjectionPointsTransformer deprecations in OIDC extension --- .../oidc/deployment/OidcBuildStep.java | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) 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 ee5af7e5b9afc..f47a1df8d4432 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(); } } }); From bd03e6cdc8936cddd588bb262ed1c7fcb8a230de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sun, 10 Nov 2024 12:57:36 +0100 Subject: [PATCH 18/23] Include allowed roles in security scheme scopes in OpenApi 3.1+ --- .../deployment/SmallRyeOpenApiProcessor.java | 7 +- .../deployment/filter/OperationFilter.java | 6 +- .../AutoSecurityRolesAllowedTestCase.java | 22 ++- ...RolesAllowedUnsupportedScopesTestCase.java | 183 ++++++++++++++++++ ...rityRolesAllowedWithInterfaceTestCase.java | 8 +- 5 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java 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 2345f482c5f80..3fc9752fd8f91 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 f33c0e56460bd..640a919febf5d 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 7a95fe1aaf5f3..ecbd54b7f603b 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 0000000000000..fd7f6b9244352 --- /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 086456e1f0757..c2063d53f6c88 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") From 686cc7d2ff00dfb69ea21ce98893b202851a3de0 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Wed, 6 Nov 2024 16:28:07 +0100 Subject: [PATCH 19/23] Propose ADR for restructuring Quarkus workshops organization - Suggests moving from a single repository to individual repositories per workshop - Aims to simplify management, improve discoverability, and enable GitHub Pages for documentation - Addresses limitations of current structure in hosting and maintaining multiple workshops --- adr/0007-quarkus-workshop-structure.adoc | 81 ++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 adr/0007-quarkus-workshop-structure.adoc diff --git a/adr/0007-quarkus-workshop-structure.adoc b/adr/0007-quarkus-workshop-structure.adoc new file mode 100644 index 0000000000000..9bfd794156568 --- /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. From 95fea0b5da08c9ec968bfb8e3b42c621ce4412cb Mon Sep 17 00:00:00 2001 From: Ales Justin Date: Thu, 17 Oct 2024 12:50:53 +0200 Subject: [PATCH 20/23] Add SimpleSpanProcessor support --- .../_includes/opentelemetry-config.adoc | 8 +++ .../deployment/OpenTelemetryProcessor.java | 1 + .../exporter/otlp/OtlpExporterProcessor.java | 11 +++-- .../OpenTelemetryDisabledSdkTest.java | 6 +-- .../otlp/OtlpTraceExporterDisabledTest.java | 6 +-- .../OpenTelemetrySamplerConfigTest.java | 4 +- ...uredOpenTelemetrySdkBuilderCustomizer.java | 49 ++++++++++++++++++- .../runtime/config/build/OTelBuildConfig.java | 11 +++++ .../exporter/otlp/OTelExporterRecorder.java | 39 +++++++++------ ...essor.java => LateBoundSpanProcessor.java} | 15 +++--- ...RemoveableLateBoundBatchSpanProcessor.java | 16 ------ .../RemoveableLateBoundSpanProcessor.java | 16 ++++++ .../vertx/exporter/SimpleProfile.java | 12 +++++ .../SimpleGrpcNoTLSNoCompressionTest.java | 15 ++++++ .../SimpleGrpcNoTLSWithCompressionTest.java | 16 ++++++ .../SimpleGrpcWithTLSNoCompressionTest.java | 16 ++++++ .../SimpleGrpcWithTLSWithCompressionTest.java | 19 +++++++ ...ithTLSWithTrustAllWithCompressionTest.java | 32 ++++++++++++ .../SimpleHttpNoTLSNoCompressionTest.java | 16 ++++++ .../SimpleHttpNoTLSWithCompressionTest.java | 19 +++++++ .../SimpleHttpWithTLSNoCompressionTest.java | 19 +++++++ .../SimpleHttpWithTLSWithCompressionTest.java | 20 ++++++++ ...thTLSWithCompressionUsingRegistryTest.java | 21 ++++++++ ...ithTLSWithTrustAllWithCompressionTest.java | 33 +++++++++++++ 24 files changed, 368 insertions(+), 52 deletions(-) rename extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/{LateBoundBatchSpanProcessor.java => LateBoundSpanProcessor.java} (84%) delete mode 100644 extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java create mode 100644 extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundSpanProcessor.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/SimpleProfile.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSNoCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSNoCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithTrustAllWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSNoCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSNoCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionUsingRegistryTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithTrustAllWithCompressionTest.java diff --git a/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc b/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc index cf4dcfff15f79..8eb3b1ba481e2 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/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 5d34b1c7c4e69..4b1846f90a857 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 e33d6271665b4..6fc49278641f0 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 a547f90bef18f..5c7faccb33257 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 7d9e074025c18..1a0372f194396 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 86218be707ce6..f9c9e4c174772 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 e601962c7b01c..eac3ccb8c1e89 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 c724ea9d640e7..bd47ccc601ec8 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 65f80415778bd..57cd1f9d2253e 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 90318940e3497..94663996276c0 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 d8654e5ff634e..0000000000000 --- 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 0000000000000..9d04b984d7d51 --- /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/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 0000000000000..b0ea9d04a013e --- /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 0000000000000..0c82fae42e1ef --- /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 0000000000000..407fd02c01fa7 --- /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 0000000000000..8c003fdacf795 --- /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 0000000000000..b764521146882 --- /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 0000000000000..45ce4303120f3 --- /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 0000000000000..f623ed071d58b --- /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 0000000000000..8fbc960b5e9be --- /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 0000000000000..a5fb15970a28c --- /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 0000000000000..77188c2cf61f5 --- /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 0000000000000..31a896345339f --- /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 0000000000000..887ab7c133df4 --- /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"))); + } + } + +} From 0878a19dabd97cdfaaebff060eac016efc0f4430 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Mon, 11 Nov 2024 13:37:42 +0000 Subject: [PATCH 21/23] Propagate Runtime properties in JBang Dev mode --- .../runner/bootstrap/AugmentActionImpl.java | 1 + .../jbang/JBangDevModeLauncherImpl.java | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) 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 9e252dcb0fb33..50f315897cd65 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/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 207281a09cb35..49dbca6aee629 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; + } } From 8ce3dc5d4189ddd656e6f7394af299170e08be49 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 11 Nov 2024 14:56:20 +0100 Subject: [PATCH 22/23] QuteErrorPageSetup: support templates that are not backed by a file - fixes #44412 --- .../qute/deployment/QuteDevModeProcessor.java | 31 +++++++++++++++++++ .../runtime/devmode/QuteErrorPageSetup.java | 17 ++++++++++ 2 files changed, 48 insertions(+) create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java 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 0000000000000..4baf3b0756616 --- /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/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 b7ced362defac..916522c98443f 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"); } From f23faa26f7ac7926a18bbc1eaf9af5b5052e8f8a Mon Sep 17 00:00:00 2001 From: mariofusco Date: Mon, 11 Nov 2024 17:47:29 +0100 Subject: [PATCH 23/23] Fix deserialization of null maps in reflection-free Jackson deserializers --- .../processor/JacksonDeserializerFactory.java | 16 +++++----- .../jackson/deployment/test/MapWrapper.java | 32 +++++++++++++++++++ .../deployment/test/SimpleJsonResource.java | 7 ++++ .../deployment/test/SimpleJsonTest.java | 16 +++++++++- ...JsonWithReflectionFreeSerializersTest.java | 2 +- 5 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java 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 fa1fdb002bf0a..ebc47ab056de6 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/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 0000000000000..6bc5bda55d642 --- /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 855c43625c09e..861f01ce08a96 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 d2f22569f9a7a..a5fa4d498c923 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 65dec05aa59a4..10ea3d373ce91 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" +