From a8ecb34a02a9ff29723b7040a8cfb3cdee7c1f93 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Wed, 25 Oct 2023 19:04:40 +0100 Subject: [PATCH 01/68] Filter configuration names from the quarkus namespace in the Gradle plugin --- .../tasks/AbstractQuarkusExtension.java | 31 +++++++++++++++++++ .../gradle/tasks/QuarkusBuildTask.java | 13 ++++++-- docs/src/main/asciidoc/reaugmentation.adoc | 8 +++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java index 8d59abd2a90ef9..f2696a9266f233 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java @@ -130,6 +130,37 @@ private EffectiveConfig buildEffectiveConfiguration(Map properti .build(); } + /** + * Filters resolved Gradle configuration for properties in the Quarkus namespace + * (as in start with quarkus.). This avoids exposing configuration that may contain secrets or + * passwords not related to Quarkus (for instance environment variables storing sensitive data for other systems). + * + * @param appArtifact the application dependency to retrive the quarkus application name and version. + * @return a filtered view of the configuration only with quarkus. names. + */ + protected Map buildSystemProperties(ResolvedDependency appArtifact) { + Map buildSystemProperties = new HashMap<>(); + buildSystemProperties.putIfAbsent("quarkus.application.name", appArtifact.getArtifactId()); + buildSystemProperties.putIfAbsent("quarkus.application.version", appArtifact.getVersion()); + + for (Map.Entry entry : forcedPropertiesProperty.get().entrySet()) { + if (entry.getKey().startsWith("quarkus.")) { + buildSystemProperties.put(entry.getKey(), entry.getValue()); + } + } + for (Map.Entry entry : quarkusBuildProperties.get().entrySet()) { + if (entry.getKey().startsWith("quarkus.")) { + buildSystemProperties.put(entry.getKey(), entry.getValue()); + } + } + for (Map.Entry entry : project.getProperties().entrySet()) { + if (entry.getKey().startsWith("quarkus.") && entry.getValue() != null) { + buildSystemProperties.put(entry.getKey(), entry.getValue().toString()); + } + } + return buildSystemProperties; + } + private String quarkusProfile() { String profile = System.getProperty(QUARKUS_PROFILE); if (profile == null) { diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index 106bca49411305..13c97cfd722a18 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @@ -205,14 +206,20 @@ void generateBuild() { }); ApplicationModel appModel = resolveAppModelForBuild(); - Map configMap = extension().buildEffectiveConfiguration(appModel.getAppArtifact()).configMap(); + Map configMap = new HashMap<>(); + for (Map.Entry entry : extension().buildEffectiveConfiguration(appModel.getAppArtifact()).configMap() + .entrySet()) { + if (entry.getKey().startsWith("quarkus.")) { + configMap.put(entry.getKey(), entry.getValue()); + } + } getLogger().info("Starting Quarkus application build for package type {}", packageType); if (getLogger().isEnabled(LogLevel.INFO)) { getLogger().info("Effective properties: {}", configMap.entrySet().stream() - .filter(e -> e.getKey().startsWith("quarkus.")).map(Object::toString) + .map(Object::toString) .sorted() .collect(Collectors.joining("\n ", "\n ", ""))); } @@ -220,7 +227,7 @@ void generateBuild() { WorkQueue workQueue = workQueue(configMap, () -> extension().buildForkOptions); workQueue.submit(BuildWorker.class, params -> { - params.getBuildSystemProperties().putAll(configMap); + params.getBuildSystemProperties().putAll(extension().buildSystemProperties(appModel.getAppArtifact())); params.getBaseName().set(extension().finalName()); params.getTargetDirectory().set(buildDir.toFile()); params.getAppModel().set(appModel); diff --git a/docs/src/main/asciidoc/reaugmentation.adoc b/docs/src/main/asciidoc/reaugmentation.adoc index 4e36e653dad3df..0c5c976565bdc1 100644 --- a/docs/src/main/asciidoc/reaugmentation.adoc +++ b/docs/src/main/asciidoc/reaugmentation.adoc @@ -21,7 +21,6 @@ Initialization steps that used to happen when an EAR file was deployed on a Jaka CDI beans added after augmentation won't work (because of the missing proxy classes) as well as build time properties (e.g. `quarkus.datasource.db-kind`) changed after augmentation will be ignored. Build time properties are marked with a lock icon (icon:lock[]) in the xref:all-config.adoc[list of all configuration options]. It doesn't matter if you use profiles or any other way to override the properties. -The build time properties that were active during augmentation are baked into the build. > Re-augmentation is the process of recreating the augmentation output for a different build time configuration @@ -33,7 +32,7 @@ If there are only two or three build time properties that depend on the user env Please notice that you won't be able to use native images with the package type `mutable-jar`. Think of the consequences and what other options you have! -It is not a good idea to do re-augmentation at runtime unless you miss the good old times when starting up a server took several minutes and you could enjoy a cup of coffee until it was ready. +It is not a good idea to do re-augmentation at runtime unless you miss the good old times when starting up a server took several minutes, and you could enjoy a cup of coffee until it was ready. == How to re-augment a Quarkus application @@ -46,6 +45,11 @@ TIP: By default, you'll get a warning if a build time property has been changed You may set the `quarkus.configuration.build-time-mismatch-at-runtime=fail` property to make sure your application does not start up if there is a mismatch. However, as of this writing changing `quarkus.datasource.db-kind` at runtime did neither fail nor produce a warning but was silently ignored. +WARNING: Build time configuration provided by build tools (`properties` in Maven `pom.xml` or `gradle.properties` +in Gradle) in the `quarkus` namespace will be part of the `mutable-jar` distribution, including configuration from +`quarkus` that reference secrets or passwords. Please, do not include sensitive information in the build tool +configuration files. + === 1. Build your application as `mutable-jar` [source,bash] From 19ce35fb771e6a4d2aac5d9b3b1a011bfecfb591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Mon, 6 Nov 2023 19:39:22 +0100 Subject: [PATCH 02/68] Move HTTP Permissions and Roles policies to runtime --- .../deployment/HttpSecurityProcessor.java | 198 +++------------ .../ManagementInterfaceSecurityProcessor.java | 54 +--- .../vertx/http/runtime/AuthConfig.java | 13 - .../vertx/http/runtime/AuthRuntimeConfig.java | 25 ++ .../vertx/http/runtime/HttpConfiguration.java | 5 + .../vertx/http/runtime/PolicyConfig.java | 1 + .../management/ManagementAuthConfig.java | 15 -- .../ManagementInterfaceConfiguration.java | 5 + .../ManagementInterfaceSecurityRecorder.java | 33 +-- .../ManagementRuntimeAuthConfig.java | 27 ++ ...bstractPathMatchingHttpSecurityPolicy.java | 174 ++++++++++++- .../security/FormAuthenticationMechanism.java | 52 ++++ .../security/HttpSecurityRecorder.java | 234 ++---------------- ...agementPathMatchingHttpSecurityPolicy.java | 13 + .../PathMatchingHttpSecurityPolicy.java | 25 +- 15 files changed, 386 insertions(+), 488 deletions(-) create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index 84b2aaca206dcd..fb34fd418ef281 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -1,28 +1,21 @@ package io.quarkus.vertx.http.deployment; import static io.quarkus.arc.processor.DotNames.APPLICATION_SCOPED; +import static io.quarkus.arc.processor.DotNames.SINGLETON; -import java.security.Permission; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Singleton; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -30,185 +23,70 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.configuration.ConfigurationException; -import io.quarkus.security.StringPermission; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.PolicyConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; -import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder; import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.SupplierImpl; import io.quarkus.vertx.http.runtime.security.VertxBlockingSecurityExecutor; import io.vertx.core.http.ClientAuth; import io.vertx.ext.web.RoutingContext; public class HttpSecurityProcessor { - @BuildStep @Record(ExecutionTime.STATIC_INIT) - public void builtins(BuildProducer producer, - BuildProducer reflectiveClassProducer, - CombinedIndexBuildItem combinedIndexBuildItem, - HttpBuildTimeConfig buildTimeConfig, HttpSecurityRecorder recorder, - BuildProducer beanProducer) { - producer.produce(new HttpSecurityPolicyBuildItem("deny", new SupplierImpl<>(new DenySecurityPolicy()))); - producer.produce(new HttpSecurityPolicyBuildItem("permit", new SupplierImpl<>(new PermitSecurityPolicy()))); - producer.produce( - new HttpSecurityPolicyBuildItem("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy()))); - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); - } - Map> permClassToCreator = new HashMap<>(); - for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { - PolicyConfig policyConfig = e.getValue(); - if (policyConfig.permissions.isEmpty()) { - producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), - new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed)))); - } else { - // create HTTP Security policy that checks allowed roles and grants SecurityIdentity permissions to - // requests that this policy allows to proceed - var permissionCreator = permClassToCreator.computeIfAbsent(policyConfig.permissionClass, - new Function>() { - @Override - public BiFunction apply(String s) { - if (StringPermission.class.getName().equals(s)) { - return recorder.stringPermissionCreator(); - } - boolean constructorAcceptsActions = validateConstructor(combinedIndexBuildItem.getIndex(), - policyConfig.permissionClass); - return recorder.customPermissionCreator(s, constructorAcceptsActions); - } - }); - var policy = recorder.createRolesAllowedPolicy(policyConfig.rolesAllowed, policyConfig.permissions, - permissionCreator); - producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), policy)); - } - } - - if (!permClassToCreator.isEmpty()) { - // we need to register Permission classes for reflection as strictly speaking - // they might not exactly match classes defined via `PermissionsAllowed#permission` - var permissionClassesArr = permClassToCreator.keySet().toArray(new String[0]); - reflectiveClassProducer - .produce(ReflectiveClassBuildItem.builder(permissionClassesArr).constructors().fields().methods().build()); - } - } - - private static boolean validateConstructor(IndexView index, String permissionClass) { - ClassInfo classInfo = index.getClassByName(permissionClass); - - if (classInfo == null) { - throw new ConfigurationException(String.format("Permission class '%s' is missing", permissionClass)); - } - - // must have exactly one constructor - if (classInfo.constructors().size() != 1) { - throw new ConfigurationException( - String.format("Permission class '%s' must have exactly one constructor", permissionClass)); - } - MethodInfo constructor = classInfo.constructors().get(0); - - // first parameter must be permission name (String) - if (constructor.parametersCount() == 0 || !isString(constructor.parameterType(0))) { - throw new ConfigurationException( - String.format("Permission class '%s' constructor first parameter must be '%s' (permission name)", - permissionClass, String.class.getName())); - } - - // second parameter (actions) is optional - if (constructor.parametersCount() == 1) { - // permission constructor accepts just name, no actions - return false; - } - - if (constructor.parametersCount() == 2) { - if (!isStringArray(constructor.parameterType(1))) { - throw new ConfigurationException( - String.format("Permission class '%s' constructor second parameter must be '%s' array", permissionClass, - String.class.getName())); - } - return true; + @BuildStep + void produceNamedHttpSecurityPolicies(List httpSecurityPolicyBuildItems, + HttpSecurityRecorder recorder) { + if (!httpSecurityPolicyBuildItems.isEmpty()) { + recorder.setBuildTimeNamedPolicies(httpSecurityPolicyBuildItems.stream().collect( + Collectors.toMap(HttpSecurityPolicyBuildItem::getName, HttpSecurityPolicyBuildItem::getPolicySupplier))); } - - throw new ConfigurationException(String.format( - "Permission class '%s' constructor must accept either one parameter (String permissionName), or two parameters (String permissionName, String[] actions)", - permissionClass)); - } - - private static boolean isStringArray(Type type) { - return type.kind() == Type.Kind.ARRAY && isString(type.asArrayType().constituent()); - } - - private static boolean isString(Type type) { - return type.kind() == Type.Kind.CLASS && type.name().toString().equals(String.class.getName()); } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initFormAuth( + @Record(ExecutionTime.STATIC_INIT) + AdditionalBeanBuildItem initFormAuth( HttpSecurityRecorder recorder, HttpBuildTimeConfig buildTimeConfig, BuildProducer filterBuildItemBuildProducer) { - if (!buildTimeConfig.auth.proactive) { - filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(buildTimeConfig.auth.form.postLocation) - .handler(recorder.formAuthPostHandler()).build()); - } if (buildTimeConfig.auth.form.enabled) { - return SyntheticBeanBuildItem.configure(FormAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupFormAuth()).done(); + if (!buildTimeConfig.auth.proactive) { + filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(buildTimeConfig.auth.form.postLocation) + .handler(recorder.formAuthPostHandler()).build()); + } + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(FormAuthenticationMechanism.class) + .setDefaultScope(SINGLETON).build(); } return null; } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initMtlsClientAuth( - HttpSecurityRecorder recorder, - HttpBuildTimeConfig buildTimeConfig) { + AdditionalBeanBuildItem initMtlsClientAuth(HttpBuildTimeConfig buildTimeConfig) { if (isMtlsClientAuthenticationEnabled(buildTimeConfig)) { - return SyntheticBeanBuildItem.configure(MtlsAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupMtlsClientAuth()).done(); + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(MtlsAuthenticationMechanism.class) + .setDefaultScope(SINGLETON).build(); } return null; } - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) + @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem initBasicAuth( HttpSecurityRecorder recorder, HttpBuildTimeConfig buildTimeConfig, - ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, BuildProducer securityInformationProducer) { - if (!applicationBasicAuthRequired(buildTimeConfig, managementInterfaceBuildTimeConfig)) { - return null; - } - SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem .configure(BasicAuthenticationMechanism.class) .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() .scope(Singleton.class) .supplier(recorder.setupBasicAuth(buildTimeConfig)); if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) @@ -247,18 +125,8 @@ void setupAuthenticationMechanisms( BuildProducer filterBuildItemBuildProducer, BuildProducer beanProducer, Capabilities capabilities, - BuildProducer beanContainerListenerBuildItemBuildProducer, HttpBuildTimeConfig buildTimeConfig, - List httpSecurityPolicyBuildItemList, BuildProducer securityInformationProducer) { - Map> policyMap = new HashMap<>(); - for (HttpSecurityPolicyBuildItem e : httpSecurityPolicyBuildItemList) { - if (policyMap.containsKey(e.getName())) { - throw new RuntimeException("Multiple HTTP security policies defined with name " + e.getName()); - } - policyMap.put(e.getName(), e.policySupplier); - } - if (!buildTimeConfig.auth.form.enabled && buildTimeConfig.auth.basic.orElse(false)) { securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } @@ -270,21 +138,13 @@ void setupAuthenticationMechanisms( beanProducer .produce(AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(HttpAuthenticator.class) .addBeanClass(HttpAuthorizer.class).build()); + beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); filterBuildItemBuildProducer .produce(new FilterBuildItem( recorder.authenticationMechanismHandler(buildTimeConfig.auth.proactive), FilterBuildItem.AUTHENTICATION)); filterBuildItemBuildProducer .produce(new FilterBuildItem(recorder.permissionCheckHandler(), FilterBuildItem.AUTHORIZATION)); - - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanContainerListenerBuildItemBuildProducer - .produce(new BeanContainerListenerBuildItem(recorder.initPermissions(buildTimeConfig, policyMap))); - } - } else { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - throw new IllegalStateException("HTTP permissions have been set however security is not enabled"); - } } } @@ -325,4 +185,18 @@ void produceEagerSecurityInterceptorStorage(HttpSecurityRecorder recorder, private static boolean isMtlsClientAuthenticationEnabled(HttpBuildTimeConfig buildTimeConfig) { return !ClientAuth.NONE.equals(buildTimeConfig.tlsClientAuth); } + + static class IsApplicationBasicAuthRequired implements BooleanSupplier { + private final boolean required; + + public IsApplicationBasicAuthRequired(HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { + required = applicationBasicAuthRequired(httpBuildTimeConfig, managementInterfaceBuildTimeConfig); + } + + @Override + public boolean getAsBoolean() { + return required; + } + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java index 07d36632f97ae4..fc7d1e31a0b6b2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java @@ -1,13 +1,8 @@ package io.quarkus.vertx.http.deployment; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Supplier; - import jakarta.inject.Singleton; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -15,47 +10,26 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.PolicyConfig; +import io.quarkus.vertx.http.deployment.HttpSecurityProcessor.IsApplicationBasicAuthRequired; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceSecurityRecorder; -import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer; import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.SupplierImpl; public class ManagementInterfaceSecurityProcessor { - @BuildStep - public void builtins(ManagementInterfaceBuildTimeConfig buildTimeConfig, - BuildProducer beanProducer) { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(ManagementPathMatchingHttpSecurityPolicy.class)); - } - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIfNot = IsApplicationBasicAuthRequired.class) + @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem initBasicAuth( - HttpBuildTimeConfig httpBuildTimeConfig, ManagementInterfaceSecurityRecorder recorder, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { - if (HttpSecurityProcessor.applicationBasicAuthRequired(httpBuildTimeConfig, managementInterfaceBuildTimeConfig)) { - return null; - } - if (managementInterfaceBuildTimeConfig.auth.basic.orElse(false)) { SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem .configure(BasicAuthenticationMechanism.class) .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() .scope(Singleton.class) .supplier(recorder.setupBasicAuth()); return configurator.done(); @@ -71,39 +45,21 @@ void setupAuthenticationMechanisms( BuildProducer filterBuildItemBuildProducer, BuildProducer beanProducer, Capabilities capabilities, - BuildProducer beanContainerListenerBuildItemBuildProducer, ManagementInterfaceBuildTimeConfig buildTimeConfig) { - - Map> policyMap = new HashMap<>(); - for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { - policyMap.put(e.getKey(), - new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed))); - } - policyMap.put("deny", new SupplierImpl<>(new DenySecurityPolicy())); - policyMap.put("permit", new SupplierImpl<>(new PermitSecurityPolicy())); - policyMap.put("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy())); - if (buildTimeConfig.auth.basic.orElse(false) && capabilities.isPresent(Capability.SECURITY)) { beanProducer .produce(AdditionalBeanBuildItem.builder().setUnremovable() .addBeanClass(HttpAuthenticator.class) + .addBeanClass(ManagementPathMatchingHttpSecurityPolicy.class) .addBeanClass(ManagementInterfaceHttpAuthorizer.class).build()); filterBuildItemBuildProducer .produce(new ManagementInterfaceFilterBuildItem( recorder.authenticationMechanismHandler(buildTimeConfig.auth.proactive), ManagementInterfaceFilterBuildItem.AUTHENTICATION)); filterBuildItemBuildProducer - .produce(new ManagementInterfaceFilterBuildItem(recorder.permissionCheckHandler(buildTimeConfig, policyMap), + .produce(new ManagementInterfaceFilterBuildItem(recorder.permissionCheckHandler(), ManagementInterfaceFilterBuildItem.AUTHORIZATION)); - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanContainerListenerBuildItemBuildProducer - .produce(new BeanContainerListenerBuildItem(recorder.initPermissions(buildTimeConfig, policyMap))); - } - } else { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - throw new IllegalStateException("HTTP permissions have been set however security is not enabled"); - } } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java index 3e609d66f98257..52b9017a38daaf 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java @@ -1,6 +1,5 @@ package io.quarkus.vertx.http.runtime; -import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; @@ -32,18 +31,6 @@ public class AuthConfig { @ConfigItem public Optional realm; - /** - * The HTTP permissions - */ - @ConfigItem(name = "permission") - public Map permissions; - - /** - * The HTTP role based policies - */ - @ConfigItem(name = "policy") - public Map rolePolicy; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java new file mode 100644 index 00000000000000..eee0b3f84d8970 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -0,0 +1,25 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Authentication mechanism information used for configuring HTTP auth instance for the deployment. + */ +@ConfigGroup +public class AuthRuntimeConfig { + + /** + * The HTTP permissions + */ + @ConfigItem(name = "permission") + public Map permissions; + + /** + * The HTTP role based policies + */ + @ConfigItem(name = "policy") + public Map rolePolicy; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 89ffdf53d0c198..e726692e1952b5 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -14,6 +14,11 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public class HttpConfiguration { + /** + * Authentication configuration + */ + public AuthRuntimeConfig auth; + /** * Enable the CORS filter. */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java index 7d16bc2f4e392d..6977b99f08770b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java @@ -36,6 +36,7 @@ public class PolicyConfig { * Permissions granted by this policy will be created with a `java.security.Permission` implementation * specified by this configuration property. The permission class must declare exactly one constructor * that accepts permission name (`String`) or permission name and actions (`String`, `String[]`). + * Permission class must be registered for reflection if you run your application in a native mode. */ @ConfigItem(defaultValue = "io.quarkus.security.StringPermission") public String permissionClass = StringPermission.class.getName(); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java index 017fcfe953a666..a22db7e1393599 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java @@ -1,12 +1,9 @@ package io.quarkus.vertx.http.runtime.management; -import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.vertx.http.runtime.PolicyConfig; -import io.quarkus.vertx.http.runtime.PolicyMappingConfig; /** * Authentication for the management interface. @@ -20,18 +17,6 @@ public class ManagementAuthConfig { @ConfigItem public Optional basic; - /** - * The HTTP permissions - */ - @ConfigItem(name = "permission") - public Map permissions; - - /** - * The HTTP role based policies - */ - @ConfigItem(name = "policy") - public Map rolePolicy; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java index 9d77f458d1c901..7a2236a17f240a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java @@ -23,6 +23,11 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "management") public class ManagementInterfaceConfiguration { + /** + * Authentication configuration + */ + public ManagementRuntimeAuthConfig auth; + /** * The HTTP port */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java index 03e6be2d83742e..f3b1b9f5a3c331 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java @@ -1,18 +1,13 @@ package io.quarkus.vertx.http.runtime.management; -import java.util.Map; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; -import io.quarkus.arc.runtime.BeanContainer; -import io.quarkus.arc.runtime.BeanContainerListener; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.vertx.http.runtime.security.AbstractPathMatchingHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.AbstractAuthenticationHandler; import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer; import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy; @@ -22,21 +17,11 @@ @Recorder public class ManagementInterfaceSecurityRecorder { - final RuntimeValue httpConfiguration; - final ManagementInterfaceBuildTimeConfig buildTimeConfig; - - public ManagementInterfaceSecurityRecorder(RuntimeValue httpConfiguration, - ManagementInterfaceBuildTimeConfig buildTimeConfig) { - this.httpConfiguration = httpConfiguration; - this.buildTimeConfig = buildTimeConfig; - } - public Handler authenticationMechanismHandler(boolean proactiveAuthentication) { return new ManagementAuthenticationHandler(proactiveAuthentication); } - public Handler permissionCheckHandler(ManagementInterfaceBuildTimeConfig buildTimeConfig, - Map> policies) { + public Handler permissionCheckHandler() { return new Handler() { volatile ManagementInterfaceHttpAuthorizer authorizer; @@ -52,17 +37,6 @@ public void handle(RoutingContext event) { }; } - public BeanContainerListener initPermissions(ManagementInterfaceBuildTimeConfig buildTimeConfig, - Map> policies) { - return new BeanContainerListener() { - @Override - public void created(BeanContainer container) { - container.beanInstance(ManagementPathMatchingHttpSecurityPolicy.class) - .init(buildTimeConfig.auth.permissions, policies, buildTimeConfig.rootPath); - } - }; - } - public Supplier setupBasicAuth() { return new Supplier() { @Override @@ -91,5 +65,10 @@ protected void setPathMatchingPolicy(RoutingContext event) { event.put(AbstractPathMatchingHttpSecurityPolicy.class.getName(), pathMatchingPolicy); } } + + @Override + protected boolean httpPermissionsEmpty() { + return CDI.current().select(ManagementInterfaceConfiguration.class).get().auth.permissions.isEmpty(); + } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java new file mode 100644 index 00000000000000..f9002d619a0815 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java @@ -0,0 +1,27 @@ +package io.quarkus.vertx.http.runtime.management; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.vertx.http.runtime.PolicyConfig; +import io.quarkus.vertx.http.runtime.PolicyMappingConfig; + +/** + * Authentication for the management interface. + */ +@ConfigGroup +public class ManagementRuntimeAuthConfig { + + /** + * The HTTP permissions + */ + @ConfigItem(name = "permission") + public Map permissions; + + /** + * The HTTP role based policies + */ + @ConfigItem(name = "policy") + public Map rolePolicy; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java index 14aa4d607cd29e..fa82007a034a3a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java @@ -1,6 +1,11 @@ package io.quarkus.vertx.http.runtime.security; +import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR; + +import java.lang.reflect.InvocationTargetException; +import java.security.Permission; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -8,9 +13,11 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; -import java.util.function.Supplier; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.PolicyConfig; import io.quarkus.vertx.http.runtime.PolicyMappingConfig; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.AuthorizationRequestContext; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult; @@ -26,6 +33,11 @@ public class AbstractPathMatchingHttpSecurityPolicy { private final PathMatcher> pathMatcher = new PathMatcher<>(); + AbstractPathMatchingHttpSecurityPolicy(Map permissions, + Map rolePolicy, String rootPath, Map namedBuildTimePolicies) { + init(permissions, toNamedHttpSecPolicies(rolePolicy, namedBuildTimePolicies), rootPath); + } + public String getAuthMechanismName(RoutingContext routingContext) { PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { @@ -79,13 +91,8 @@ public Uni apply(CheckResult checkResult) { }); } - public void init(Map permissions, - Map> supplierMap, String rootPath) { - Map permissionCheckers = new HashMap<>(); - for (Map.Entry> i : supplierMap.entrySet()) { - permissionCheckers.put(i.getKey(), i.getValue().get()); - } - + private void init(Map permissions, + Map permissionCheckers, String rootPath) { Map> tempMap = new HashMap<>(); for (Map.Entry entry : permissions.entrySet()) { HttpSecurityPolicy checker = permissionCheckers.get(entry.getValue().policy); @@ -150,6 +157,157 @@ public List findPermissionCheckers(RoutingContext context) { } + private static Map toNamedHttpSecPolicies(Map rolePolicies, + Map namedBuildTimePolicies) { + Map namedPolicies = new HashMap<>(); + if (!namedBuildTimePolicies.isEmpty()) { + namedPolicies.putAll(namedBuildTimePolicies); + } + for (Map.Entry e : rolePolicies.entrySet()) { + PolicyConfig policyConfig = e.getValue(); + if (policyConfig.permissions.isEmpty()) { + namedPolicies.put(e.getKey(), new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed)); + } else { + final Map> roleToPermissions = new HashMap<>(); + for (Map.Entry> roleToPermissionStr : policyConfig.permissions.entrySet()) { + + // collect permission actions + // perm1:action1,perm2:action2,perm1:action3 -> perm1:action1,action3 and perm2:action2 + Map cache = new HashMap<>(); + final String role = roleToPermissionStr.getKey(); + for (String permissionToAction : roleToPermissionStr.getValue()) { + // parse permission to actions and add it to cache + addPermissionToAction(cache, role, permissionToAction); + } + + // create permissions + var permissions = new HashSet(); + for (PermissionToActions helper : cache.values()) { + if (StringPermission.class.getName().equals(policyConfig.permissionClass)) { + permissions.add(new StringPermission(helper.permissionName, helper.actions.toArray(new String[0]))); + } else { + permissions.add(customPermissionCreator(policyConfig, helper)); + } + } + + roleToPermissions.put(role, Set.copyOf(permissions)); + } + namedPolicies.put(e.getKey(), + new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed, Map.copyOf(roleToPermissions))); + } + } + namedPolicies.put("deny", new DenySecurityPolicy()); + namedPolicies.put("permit", new PermitSecurityPolicy()); + namedPolicies.put("authenticated", new AuthenticatedHttpSecurityPolicy()); + return namedPolicies; + } + + private static boolean acceptsActions(String permissionClassStr) { + var permissionClass = loadClass(permissionClassStr); + if (permissionClass.getConstructors().length != 1) { + throw new ConfigurationException( + String.format("Permission class '%s' must have exactly one constructor", permissionClass)); + } + var constructor = permissionClass.getConstructors()[0]; + // first parameter must be permission name (String) + if (constructor.getParameterCount() == 0 || !(constructor.getParameterTypes()[0] == String.class)) { + throw new ConfigurationException( + String.format("Permission class '%s' constructor first parameter must be '%s' (permission name)", + permissionClass, String.class.getName())); + } + final boolean acceptsActions; + if (constructor.getParameterCount() == 1) { + acceptsActions = false; + } else { + if (constructor.getParameterCount() == 2) { + if (constructor.getParameterTypes()[1] != String[].class) { + throw new ConfigurationException( + String.format("Permission class '%s' constructor second parameter must be '%s' array", + permissionClass, + String.class.getName())); + } + } else { + throw new ConfigurationException(String.format( + "Permission class '%s' constructor must accept either one parameter (String permissionName), or two parameters (String permissionName, String[] actions)", + permissionClass)); + } + acceptsActions = true; + } + return acceptsActions; + } + + private static void addPermissionToAction(Map cache, String role, String permissionToAction) { + final String permissionName; + final String action; + // incoming value is either in format perm1:action1 or perm1 (with or withot action) + if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { + // perm1:action1 + var permToActions = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); + if (permToActions.length != 2) { + throw new ConfigurationException( + String.format("Invalid permission format '%s', please use exactly one permission to action separator", + permissionToAction)); + } + permissionName = permToActions[0].trim(); + action = permToActions[1].trim(); + } else { + // perm1 + permissionName = permissionToAction.trim(); + action = null; + } + + if (permissionName.isEmpty()) { + throw new ConfigurationException( + String.format("Invalid permission name '%s' for role '%s'", permissionToAction, role)); + } + + cache.computeIfAbsent(permissionName, new Function() { + @Override + public PermissionToActions apply(String s) { + return new PermissionToActions(s); + } + }).addAction(action); + } + + private static Class loadClass(String className) { + try { + return Thread.currentThread().getContextClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e); + } + } + + private static Permission customPermissionCreator(PolicyConfig policyConfig, PermissionToActions helper) { + try { + var constructor = loadClass(policyConfig.permissionClass).getConstructors()[0]; + if (acceptsActions(policyConfig.permissionClass)) { + return (Permission) constructor.newInstance(helper.permissionName, helper.actions.toArray(new String[0])); + } else { + return (Permission) constructor.newInstance(helper.permissionName); + } + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(String.format("Failed to create Permission - class '%s', name '%s', actions '%s'", + policyConfig.permissionClass, helper.permissionName, + Arrays.toString(helper.actions.toArray(new String[0]))), e); + } + } + + private static final class PermissionToActions { + private final String permissionName; + private final Set actions; + + private PermissionToActions(String permissionName) { + this.permissionName = permissionName; + this.actions = new HashSet<>(); + } + + private void addAction(String action) { + if (action != null) { + this.actions.add(action); + } + } + } + static class HttpMatcher { final String authMechanism; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index 85927ed2f89229..48bfff2708aadb 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -1,12 +1,16 @@ package io.quarkus.vertx.http.runtime.security; import java.net.URI; +import java.security.SecureRandom; import java.util.Arrays; +import java.util.Base64; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import jakarta.inject.Inject; + import org.jboss.logging.Logger; import io.netty.handler.codec.http.HttpHeaderNames; @@ -18,6 +22,9 @@ import io.quarkus.security.identity.request.AuthenticationRequest; import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.vertx.http.runtime.FormAuthConfig; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.UniEmitter; import io.vertx.core.Handler; @@ -47,6 +54,44 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism private final PersistentLoginManager loginManager; + //the temp encryption key, persistent across dev mode restarts + static volatile String encryptionKey; + + @Inject + FormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { + String key; + if (!httpConfiguration.encryptionKey.isPresent()) { + if (encryptionKey != null) { + //persist across dev mode restarts + key = encryptionKey; + } else { + byte[] data = new byte[32]; + new SecureRandom().nextBytes(data); + key = encryptionKey = Base64.getEncoder().encodeToString(data); + log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); + } + } else { + key = httpConfiguration.encryptionKey.get(); + } + FormAuthConfig form = buildTimeConfig.auth.form; + this.loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), + form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), + form.cookiePath.orElse(null)); + this.loginPage = startWithSlash(form.loginPage.orElse(null)); + this.errorPage = startWithSlash(form.errorPage.orElse(null)); + this.landingPage = startWithSlash(form.landingPage.orElse(null)); + this.postLocation = startWithSlash(form.postLocation); + this.usernameParameter = form.usernameParameter; + this.passwordParameter = form.passwordParameter; + this.locationCookie = form.locationCookie; + this.cookiePath = form.cookiePath.orElse(null); + boolean redirectAfterLogin = form.redirectAfterLogin; + this.redirectToLandingPage = landingPage != null && redirectAfterLogin; + this.redirectToLoginPage = loginPage != null; + this.redirectToErrorPage = errorPage != null; + this.cookieSameSite = CookieSameSite.valueOf(form.cookieSameSite.name()); + } + public FormAuthenticationMechanism(String loginPage, String postLocation, String usernameParameter, String passwordParameter, String errorPage, String landingPage, boolean redirectAfterLogin, String locationCookie, String cookieSameSite, String cookiePath, @@ -240,4 +285,11 @@ public Set> getCredentialTypes() { public Uni getCredentialTransport(RoutingContext context) { return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM)); } + + private static String startWithSlash(String page) { + if (page == null) { + return null; + } + return page.startsWith("/") ? page : "/" + page; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 84b3ef16064ade..40cc75234ddce4 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -1,20 +1,9 @@ package io.quarkus.vertx.http.runtime.security; -import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR; - -import java.lang.reflect.InvocationTargetException; -import java.security.Permission; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.Base64; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CompletionException; import java.util.function.BiConsumer; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -24,19 +13,14 @@ import org.jboss.logging.Logger; -import io.quarkus.arc.runtime.BeanContainer; -import io.quarkus.arc.runtime.BeanContainerListener; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; -import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.MethodDescription; -import io.quarkus.vertx.http.runtime.FormAuthConfig; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.CompositeException; @@ -52,23 +36,6 @@ public class HttpSecurityRecorder { private static final Logger log = Logger.getLogger(HttpSecurityRecorder.class); - protected static final Consumer NOOP_CALLBACK = new Consumer() { - @Override - public void accept(Throwable throwable) { - - } - }; - - final RuntimeValue httpConfiguration; - final HttpBuildTimeConfig buildTimeConfig; - - //the temp encryption key, persistent across dev mode restarts - static volatile String encryptionKey; - - public HttpSecurityRecorder(RuntimeValue httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { - this.httpConfiguration = httpConfiguration; - this.buildTimeConfig = buildTimeConfig; - } public Handler authenticationMechanismHandler(boolean proactiveAuthentication) { return new HttpAuthenticationHandler(proactiveAuthentication); @@ -88,64 +55,6 @@ public void handle(RoutingContext event) { }; } - public BeanContainerListener initPermissions(HttpBuildTimeConfig buildTimeConfig, - Map> policies) { - return new BeanContainerListener() { - @Override - public void created(BeanContainer container) { - container.beanInstance(PathMatchingHttpSecurityPolicy.class) - .init(buildTimeConfig.auth.permissions, policies, buildTimeConfig.rootPath); - } - }; - } - - public Supplier setupFormAuth() { - - return new Supplier() { - - @Override - public FormAuthenticationMechanism get() { - String key; - if (!httpConfiguration.getValue().encryptionKey.isPresent()) { - if (encryptionKey != null) { - //persist across dev mode restarts - key = encryptionKey; - } else { - byte[] data = new byte[32]; - new SecureRandom().nextBytes(data); - key = encryptionKey = Base64.getEncoder().encodeToString(data); - log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); - } - } else { - key = httpConfiguration.getValue().encryptionKey.get(); - } - FormAuthConfig form = buildTimeConfig.auth.form; - PersistentLoginManager loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), - form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), - form.cookiePath.orElse(null)); - String loginPage = startWithSlash(form.loginPage.orElse(null)); - String errorPage = startWithSlash(form.errorPage.orElse(null)); - String landingPage = startWithSlash(form.landingPage.orElse(null)); - String postLocation = startWithSlash(form.postLocation); - String usernameParameter = form.usernameParameter; - String passwordParameter = form.passwordParameter; - String locationCookie = form.locationCookie; - String cookiePath = form.cookiePath.orElse(null); - boolean redirectAfterLogin = form.redirectAfterLogin; - return new FormAuthenticationMechanism(loginPage, postLocation, usernameParameter, passwordParameter, - errorPage, landingPage, redirectAfterLogin, locationCookie, form.cookieSameSite.name(), cookiePath, - loginManager); - } - }; - } - - private static String startWithSlash(String page) { - if (page == null) { - return null; - } - return page.startsWith("/") ? page : "/" + page; - } - public Supplier setupBasicAuth(HttpBuildTimeConfig buildTimeConfig) { return new Supplier() { @Override @@ -156,15 +65,6 @@ public BasicAuthenticationMechanism get() { }; } - public Supplier setupMtlsClientAuth() { - return new Supplier() { - @Override - public MtlsAuthenticationMechanism get() { - return new MtlsAuthenticationMechanism(); - } - }; - } - /** * This handler resolves the identity, and will be mapped to the post location. Otherwise, * for lazy auth the post will not be evaluated if there is no security rule for the post location. @@ -194,36 +94,6 @@ public void onFailure(Throwable throwable) { }; } - public BiFunction stringPermissionCreator() { - return StringPermission::new; - } - - public BiFunction customPermissionCreator(String clazz, boolean acceptsActions) { - return new BiFunction() { - @Override - public Permission apply(String name, String[] actions) { - try { - if (acceptsActions) { - return (Permission) loadClass(clazz).getConstructors()[0].newInstance(name, actions); - } else { - return (Permission) loadClass(clazz).getConstructors()[0].newInstance(name); - } - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException( - String.format("Failed to create Permission - class '%s', name '%s', actions '%s'", clazz, - name, Arrays.toString(actions)), - e); - } - } - }; - } - - public Supplier createRolesAllowedPolicy(List rolesAllowed, - Map> roleToPermissionsStr, BiFunction permissionCreator) { - final Map> roleToPermissions = createPermissions(roleToPermissionsStr, permissionCreator); - return new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(rolesAllowed, roleToPermissions)); - } - public Supplier createSecurityInterceptorStorage( Map, Consumer> endpointRuntimeValToInterceptor) { @@ -240,63 +110,12 @@ public EagerSecurityInterceptorStorage get() { }; } - private static Map> createPermissions(Map> roleToPermissions, - BiFunction permissionCreator) { - // role -> created permissions - Map> result = new HashMap<>(); - for (Map.Entry> e : roleToPermissions.entrySet()) { - - // collect permission actions - // perm1:action1,perm2:action2,perm1:action3 -> perm1:action1,action3 and perm2:action2 - Map cache = new HashMap<>(); - final String role = e.getKey(); - for (String permissionToAction : e.getValue()) { - // parse permission to actions and add it to cache - addPermissionToAction(cache, role, permissionToAction); - } - - // create permissions - var permissions = new HashSet(); - for (PermissionToActions permission : cache.values()) { - permissions.add(permission.create(permissionCreator)); - } - - result.put(role, Set.copyOf(permissions)); + public void setBuildTimeNamedPolicies(Map> buildTimeNamedPolicies) { + Map nameToPolicy = new HashMap<>(); + for (Map.Entry> nameToSupplier : buildTimeNamedPolicies.entrySet()) { + nameToPolicy.put(nameToSupplier.getKey(), nameToSupplier.getValue().get()); } - return Map.copyOf(result); - } - - private static void addPermissionToAction(Map cache, String role, String permissionToAction) { - final String permissionName; - final String action; - // incoming value is either in format perm1:action1 or perm1 (with or withot action) - if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { - // perm1:action1 - var permToActions = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); - if (permToActions.length != 2) { - throw new ConfigurationException( - String.format("Invalid permission format '%s', please use exactly one permission to action separator", - permissionToAction)); - } - permissionName = permToActions[0].trim(); - action = permToActions[1].trim(); - } else { - // perm1 - permissionName = permissionToAction.trim(); - action = null; - } - - if (permissionName.isEmpty()) { - throw new ConfigurationException( - String.format("Invalid permission name '%s' for role '%s'", permissionToAction, role)); - } - - cache.computeIfAbsent(permissionName, new Function() { - @Override - public PermissionToActions apply(String s) { - return new PermissionToActions(s); - } - }).addAction(action); + PathMatchingHttpSecurityPolicy.replaceNamedBuildTimePolicies(nameToPolicy); } public static abstract class DefaultAuthFailureHandler implements BiConsumer { @@ -380,10 +199,16 @@ protected void setPathMatchingPolicy(RoutingContext event) { event.put(AbstractPathMatchingHttpSecurityPolicy.class.getName(), pathMatchingPolicy); } } + + @Override + protected boolean httpPermissionsEmpty() { + return CDI.current().select(HttpConfiguration.class).get().auth.permissions.isEmpty(); + } } public static abstract class AbstractAuthenticationHandler implements Handler { volatile HttpAuthenticator authenticator; + volatile Boolean patchMatchingPolicyEnabled = null; final boolean proactiveAuthentication; public AbstractAuthenticationHandler(boolean proactiveAuthentication) { @@ -397,7 +222,12 @@ public void handle(RoutingContext event) { } //we put the authenticator into the routing context so it can be used by other systems event.put(HttpAuthenticator.class.getName(), authenticator); - setPathMatchingPolicy(event); + if (patchMatchingPolicyEnabled == null) { + setPatchMatchingPolicyEnabled(); + } + if (patchMatchingPolicyEnabled) { + setPathMatchingPolicy(event); + } //register the default auth failure handler if (proactiveAuthentication) { @@ -523,34 +353,14 @@ public void accept(SecurityIdentity identity, Throwable throwable, Boolean aBool } } - protected abstract void setPathMatchingPolicy(RoutingContext event); - } - - private static final class PermissionToActions { - private final String permissionName; - private final Set actions; - - private PermissionToActions(String permissionName) { - this.permissionName = permissionName; - this.actions = new HashSet<>(); - } - - private void addAction(String action) { - if (action != null) { - this.actions.add(action); + private synchronized void setPatchMatchingPolicyEnabled() { + if (patchMatchingPolicyEnabled == null) { + patchMatchingPolicyEnabled = !httpPermissionsEmpty(); } } - private Permission create(BiFunction permissionCreator) { - return permissionCreator.apply(permissionName, actions.toArray(new String[0])); - } - } + protected abstract void setPathMatchingPolicy(RoutingContext event); - private static Class loadClass(String className) { - try { - return Thread.currentThread().getContextClassLoader().loadClass(className); - } catch (ClassNotFoundException e) { - throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e); - } + protected abstract boolean httpPermissionsEmpty(); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java index fc0258841adf17..4a22ac63fa04e1 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java @@ -1,12 +1,25 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.Map; + import jakarta.inject.Singleton; +import io.quarkus.runtime.Startup; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; + /** * A security policy that allows for matching of other security policies based on paths. * * This is used for the default path/method based RBAC. */ +@Startup // do not initialize path matcher during first HTTP request @Singleton public class ManagementPathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy { + + ManagementPathMatchingHttpSecurityPolicy(ManagementInterfaceBuildTimeConfig buildTimeConfig, + ManagementInterfaceConfiguration runTimeConfig) { + super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, Map.of()); + } + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java index 2132ab9532dd35..c5624c4b6c7a4b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java @@ -1,13 +1,34 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.HashMap; +import java.util.Map; + import jakarta.inject.Singleton; +import io.quarkus.runtime.Startup; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; + /** * A security policy that allows for matching of other security policies based on paths. * * This is used for the default path/method based RBAC. */ +@Startup // do not initialize path matcher during first HTTP request @Singleton -public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy - implements HttpSecurityPolicy { +public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy implements HttpSecurityPolicy { + + // this map is planned for removal very soon as runtime named policies will make it obsolete + private static final Map HTTP_SECURITY_BUILD_TIME_POLICIES = new HashMap<>(); + + PathMatchingHttpSecurityPolicy(HttpConfiguration httpConfig, HttpBuildTimeConfig buildTimeConfig) { + super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, + HTTP_SECURITY_BUILD_TIME_POLICIES); + } + + static synchronized void replaceNamedBuildTimePolicies(Map newPolicies) { + HTTP_SECURITY_BUILD_TIME_POLICIES.clear(); + HTTP_SECURITY_BUILD_TIME_POLICIES.putAll(newPolicies); + } + } From 874c7922871076089396002d8a968c030b0ce7b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 22:15:41 +0000 Subject: [PATCH 03/68] Bump com.google.api.grpc:proto-google-common-protos Bumps [com.google.api.grpc:proto-google-common-protos](https://github.com/googleapis/sdk-platform-java) from 2.27.0 to 2.28.0. - [Release notes](https://github.com/googleapis/sdk-platform-java/releases) - [Changelog](https://github.com/googleapis/sdk-platform-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/sdk-platform-java/compare/v2.27.0...v2.28.0) --- updated-dependencies: - dependency-name: com.google.api.grpc:proto-google-common-protos dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f7323a0c9aa8d6..83366def81d3b8 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ 1.2.1 3.24.4 ${protoc.version} - 2.27.0 + 2.28.0 7.4.0 From db5d540a0ebeb9b1afb3596d015235f6e2f5eed6 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 7 Nov 2023 11:08:02 +0000 Subject: [PATCH 04/68] Rename OidcClientRequestFilter filter to OidcRequestFilter --- ...ecurity-oidc-code-flow-authentication.adoc | 4 ++-- ...urity-openid-connect-client-reference.adoc | 11 ++++++----- .../oidc/client/runtime/OidcClientImpl.java | 10 +++++----- .../client/runtime/OidcClientRecorder.java | 6 +++--- .../oidc/common/OidcClientRequestFilter.java | 17 ----------------- .../common/OidcRequestContextProperties.java | 16 ++++++++++++++++ .../oidc/common/OidcRequestFilter.java | 19 +++++++++++++++++++ .../oidc/common/runtime/OidcCommonUtils.java | 12 ++++++------ .../oidc/runtime/OidcProviderClient.java | 10 +++++----- .../io/quarkus/oidc/runtime/OidcRecorder.java | 4 ++-- .../it/keycloak/OidcRequestCustomizer.java | 7 ++++--- .../it/keycloak/OidcRequestCustomizer.java | 7 ++++--- 12 files changed, 72 insertions(+), 51 deletions(-) delete mode 100644 extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcClientRequestFilter.java create mode 100644 extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java create mode 100644 extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index c896b0ac855503..75b5aad3acc2de 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -280,9 +280,9 @@ quarkus.oidc.introspection-credentials.secret=introspection-user-secret ---- [[oidc-client-filters]] -==== OIDC client request customization +==== OIDC request customization -You can customize OIDC client requests by registering one or more `OidcClientRequestFiler` implementations which can update or add new request headers, please see xref:security-openid-connect-client-reference#oidc-client-filters[Client request customization] for more information. +You can customize OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations which can update or add new request headers, please see xref:security-openid-connect-client-reference#oidc-client-filters[Client request customization] for more information. ==== Redirecting to and from the OIDC provider diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index c45c0bb707dcf8..17dcb5a8911128 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -873,9 +873,9 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-lev ---- [[oidc-client-filters]] -== Client request customization +== OIDC request customization -You can customize OIDC client requests by registering one or more `OidcClientRequestFiler` implementations which can update or add new request headers, for example, a filter can analyze the request body and add its digest as a new header value: +You can customize OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations which can update or add new request headers, for example, a filter can analyze the request body and add its digest as a new header value: [source,java] ---- @@ -884,17 +884,18 @@ package io.quarkus.it.keycloak; import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.core.http.HttpMethod; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpRequest; @ApplicationScoped @Unremovable -public class OidcClientRequestCustomizer implements OidcClientRequestFilter { +public class OidcRequestCustomizer implements OidcRequestFilter { @Override - public void filter(HttpRequest request, Buffer buffer) { + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProperties) { HttpMethod method = request.method(); String uri = request.uri(); if (method == HttpMethod.POST && uri.endsWith("/service") && buffer != null) { diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java index 58e8018629860e..0f0252a3a003e6 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java @@ -17,7 +17,7 @@ import io.quarkus.oidc.client.OidcClientConfig; import io.quarkus.oidc.client.OidcClientException; import io.quarkus.oidc.client.Tokens; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.smallrye.mutiny.Uni; @@ -46,12 +46,12 @@ public class OidcClientImpl implements OidcClient { private final String clientSecretBasicAuthScheme; private final Key clientJwtKey; private final OidcClientConfig oidcConfig; - private final List filters; + private final List filters; private volatile boolean closed; public OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType, MultiMap tokenGrantParams, MultiMap commonRefreshGrantParams, OidcClientConfig oidcClientConfig, - List filters) { + List filters) { this.client = client; this.tokenRequestUri = tokenRequestUri; this.tokenRevokeUri = tokenRevokeUri; @@ -260,8 +260,8 @@ private void checkClosed() { } private HttpRequest filter(HttpRequest request, Buffer body) { - for (OidcClientRequestFilter filter : filters) { - filter.filter(request, body); + for (OidcRequestFilter filter : filters) { + filter.filter(request, body, null); } return request; } diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java index 771f8621401b23..88004463f2e5f0 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java @@ -17,7 +17,7 @@ import io.quarkus.oidc.client.OidcClientException; import io.quarkus.oidc.client.OidcClients; import io.quarkus.oidc.client.Tokens; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.runtime.TlsConfig; @@ -122,7 +122,7 @@ protected static Uni createOidcClientUni(OidcClientConfig oidcConfig WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx.get()), options); - List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); + List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); Uni tokenUrisUni = null; if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.tokenPath)) { @@ -211,7 +211,7 @@ private static void setGrantClientParams(OidcClientConfig oidcConfig, MultiMap g } private static Uni discoverTokenUris(WebClient client, - List clientRequestFilters, + List clientRequestFilters, String authServerUrl, OidcClientConfig oidcConfig) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); return OidcCommonUtils.discoverMetadata(client, clientRequestFilters, authServerUrl, connectionDelayInMillisecs) diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcClientRequestFilter.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcClientRequestFilter.java deleted file mode 100644 index d8d653381329a0..00000000000000 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcClientRequestFilter.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.quarkus.oidc.common; - -import io.vertx.mutiny.core.buffer.Buffer; -import io.vertx.mutiny.ext.web.client.HttpRequest; - -/** - * Request filter which can be used to customize OIDC client requests - */ -public interface OidcClientRequestFilter { - /** - * Filter OIDC client requests - * - * @param request HTTP request - * @param body request body, will be null for HTTP GET methods, may be null for other HTTP methods - */ - void filter(HttpRequest request, Buffer body); -} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java new file mode 100644 index 00000000000000..cdf75431cd09d1 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java @@ -0,0 +1,16 @@ +package io.quarkus.oidc.common; + +import java.util.Map; + +public class OidcRequestContextProperties { + + private final Map properties; + + public OidcRequestContextProperties(Map properties) { + this.properties = properties; + } + + public Object getProperty(String name) { + return properties.get(name); + } +} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java new file mode 100644 index 00000000000000..7318f34eff3b17 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java @@ -0,0 +1,19 @@ +package io.quarkus.oidc.common; + +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpRequest; + +/** + * Request filter which can be used to customize requests such as the verification JsonWebKey set and token grant requests + * which are made from the OIDC adapter to the OIDC provider + */ +public interface OidcRequestFilter { + /** + * Filter OIDC requests + * + * @param request HTTP request that can have its headers customized + * @param body request body, will be null for HTTP GET methods, may be null for other HTTP methods + * @param contextProperties context properties that can be available in context of some requests, can be null + */ + void filter(HttpRequest request, Buffer requestBody, OidcRequestContextProperties contextProperties); +} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java index f5c74bdbb4c427..3b0e9dbdc8f768 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java @@ -30,7 +30,7 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Provider; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret; @@ -427,12 +427,12 @@ public static Predicate oidcEndpointNotAvailable() { || (t instanceof OidcEndpointAccessException && ((OidcEndpointAccessException) t).getErrorStatus() == 404)); } - public static Uni discoverMetadata(WebClient client, List filters, + public static Uni discoverMetadata(WebClient client, List filters, String authServerUrl, long connectionDelayInMillisecs) { final String discoveryUrl = authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION; HttpRequest request = client.getAbs(discoveryUrl); - for (OidcClientRequestFilter filter : filters) { - filter.filter(request, null); + for (OidcRequestFilter filter : filters) { + filter.filter(request, null, null); } return request.send().onItem().transform(resp -> { if (resp.statusCode() == 200) { @@ -478,10 +478,10 @@ private static byte[] doRead(InputStream is) throws IOException { return out.toByteArray(); } - public static List getClientRequestCustomizer() { + public static List getClientRequestCustomizer() { ArcContainer container = Arc.container(); if (container != null) { - return container.listAll(OidcClientRequestFilter.class).stream().map(handle -> handle.get()) + return container.listAll(OidcRequestFilter.class).stream().map(handle -> handle.get()) .collect(Collectors.toList()); } return List.of(); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index 77ba52c65ebeb9..ed4abe2aafecfd 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -15,7 +15,7 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.common.runtime.OidcEndpointAccessException; @@ -45,12 +45,12 @@ public class OidcProviderClient implements Closeable { private final String clientSecretBasicAuthScheme; private final String introspectionBasicAuthScheme; private final Key clientJwtKey; - private final List filters; + private final List filters; public OidcProviderClient(WebClient client, OidcConfigurationMetadata metadata, OidcTenantConfig oidcConfig, - List filters) { + List filters) { this.client = client; this.metadata = metadata; this.oidcConfig = oidcConfig; @@ -220,8 +220,8 @@ public Key getClientJwtKey() { } private HttpRequest filter(HttpRequest request, Buffer body) { - for (OidcClientRequestFilter filter : filters) { - filter.filter(request, body); + for (OidcRequestFilter filter : filters) { + filter.filter(request, body, null); } return request; } 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 f53e0d0a0e23bf..d169d2bcd20794 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 @@ -30,7 +30,7 @@ import io.quarkus.oidc.OidcTenantConfig.TokenStateManager.Strategy; import io.quarkus.oidc.TenantConfigResolver; import io.quarkus.oidc.TenantIdentityProvider; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.runtime.LaunchMode; @@ -434,7 +434,7 @@ protected static Uni createOidcClientUni(OidcTenantConfig oi WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx), options); - List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); + List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); Uni metadataUni = null; if (!oidcConfig.discoveryEnabled.orElse(true)) { diff --git a/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java b/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java index 6f9df502631112..b0d0f2282c034d 100644 --- a/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java +++ b/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java @@ -3,16 +3,17 @@ import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpRequest; @ApplicationScoped @Unremovable -public class OidcRequestCustomizer implements OidcClientRequestFilter { +public class OidcRequestCustomizer implements OidcRequestFilter { @Override - public void filter(HttpRequest request, Buffer buffer) { + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProps) { String uri = request.uri(); if (uri.endsWith("/non-standard-tokens")) { request.putHeader("GrantType", getGrantType(buffer.toString())); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java index 071409f1cea53a..b26cb0aa8049cb 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java @@ -3,17 +3,18 @@ import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.core.http.HttpMethod; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpRequest; @ApplicationScoped @Unremovable -public class OidcRequestCustomizer implements OidcClientRequestFilter { +public class OidcRequestCustomizer implements OidcRequestFilter { @Override - public void filter(HttpRequest request, Buffer buffer) { + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProps) { HttpMethod method = request.method(); String uri = request.uri(); if (method == HttpMethod.GET && uri.endsWith("/auth/azure/jwk")) { From 66391b44b6d5c9266120331ceda1c8f890bd3208 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 7 Nov 2023 15:01:25 +0100 Subject: [PATCH 05/68] Qute: consider synthetic parameter declarations during validation - Template#getParameterDeclarations() now also returns type param declarations defined by parser hooks (i.e. type-safe templates and globals) --- .../qute/deployment/QuteProcessor.java | 186 +++++------------- .../TemplatesAnalysisBuildItem.java | 19 ++ .../qute/deployment/TemplateAnalysisTest.java | 87 ++++++++ .../io/quarkus/qute/ParameterDeclaration.java | 2 +- .../src/main/java/io/quarkus/qute/Parser.java | 18 +- .../java/io/quarkus/qute/ParserHelper.java | 3 +- .../src/main/java/io/quarkus/qute/Scope.java | 4 + .../main/java/io/quarkus/qute/Template.java | 5 +- .../java/io/quarkus/qute/TemplateNode.java | 7 + .../test/java/io/quarkus/qute/ParserTest.java | 12 ++ 10 files changed, 204 insertions(+), 139 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateAnalysisTest.java diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index d4e52487d5ecd1..8497e70b4dedfb 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -104,7 +104,6 @@ import io.quarkus.qute.ErrorCode; import io.quarkus.qute.Expression; import io.quarkus.qute.Expression.VirtualMethodPart; -import io.quarkus.qute.Expressions; import io.quarkus.qute.LoopSectionHelper; import io.quarkus.qute.NamespaceResolver; import io.quarkus.qute.ParameterDeclaration; @@ -121,7 +120,6 @@ import io.quarkus.qute.TemplateGlobal; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateLocator; -import io.quarkus.qute.TemplateNode; import io.quarkus.qute.UserTagSectionHelper; import io.quarkus.qute.ValueResolver; import io.quarkus.qute.Variant; @@ -657,49 +655,9 @@ public Optional getVariant() { } }); - // It's a file-based template - // We need to find out whether the parsed template represents a checked template - Map pathToPathWithoutSuffix = new HashMap<>(); - for (String path : filePaths.getFilePaths()) { - for (String suffix : config.suffixes) { - if (path.endsWith(suffix)) { - // Remove the suffix and add to Map - pathToPathWithoutSuffix.put(path, path.substring(0, path.length() - (suffix.length() + 1))); - break; - } - } - - // Path has already no suffix - if (!pathToPathWithoutSuffix.containsKey(path)) { - pathToPathWithoutSuffix.put(path, path); - } - } - - // Checked Template id -> method parameter declaration - Map> checkedTemplateIdToParamDecl = new HashMap<>(); - for (CheckedTemplateBuildItem checkedTemplate : checkedTemplates) { - if (checkedTemplate.isFragment()) { - continue; - } - for (Entry entry : checkedTemplate.bindings.entrySet()) { - checkedTemplateIdToParamDecl - .computeIfAbsent(checkedTemplate.templateId, s -> new HashMap<>()) - .put(entry.getKey(), new MethodParameterDeclaration(entry.getValue(), entry.getKey())); - } - } - - // Message Bundle Template id -> method parameter declaration - Map> msgBundleTemplateIdToParamDecl = new HashMap<>(); - for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) { - MethodInfo method = messageBundleMethod.getMethod(); - for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { - Type paramType = it.next(); - String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); - msgBundleTemplateIdToParamDecl - .computeIfAbsent(messageBundleMethod.getTemplateId(), s -> new HashMap<>()) - .put(name, new MethodParameterDeclaration(getCheckedTemplateParameterTypeName(paramType), name)); - } - } + Map messageBundleMethodsMap = messageBundleMethods.stream() + .filter(MessageBundleMethodBuildItem::isValidatable) + .collect(Collectors.toMap(MessageBundleMethodBuildItem::getTemplateId, Function.identity())); builder.addParserHook(new ParserHook() { @@ -715,8 +673,17 @@ public void beforeParsing(ParserHelper parserHelper) { getCheckedTemplateParameterTypeName(global.getVariableType()).toString()); } - addMethodParamsToParserHelper(parserHelper, pathToPathWithoutSuffix.get(templateId), - checkedTemplateIdToParamDecl); + // It's a file-based template + // We need to find out whether the parsed template represents a checked template + String path = templatePathWithoutSuffix(templateId, config); + for (CheckedTemplateBuildItem checkedTemplate : checkedTemplates) { + if (checkedTemplate.templateId.equals(path)) { + for (Entry entry : checkedTemplate.bindings.entrySet()) { + parserHelper.addParameter(entry.getKey(), entry.getValue()); + } + break; + } + } if (templateId.startsWith(TemplatePathBuildItem.TAGS)) { parserHelper.addParameter(UserTagSectionHelper.Factory.ARGS, @@ -724,7 +691,16 @@ public void beforeParsing(ParserHelper parserHelper) { } } - addMethodParamsToParserHelper(parserHelper, templateId, msgBundleTemplateIdToParamDecl); + // If needed add params to message bundle templates + MessageBundleMethodBuildItem messageBundleMethod = messageBundleMethodsMap.get(templateId); + if (messageBundleMethod != null) { + MethodInfo method = messageBundleMethod.getMethod(); + for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { + Type paramType = it.next(); + String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); + parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + } + } } }).build(); @@ -736,17 +712,7 @@ public void beforeParsing(ParserHelper parserHelper) { for (TemplatePathBuildItem path : templatePaths) { Template template = dummyEngine.getTemplate(path.getPath()); if (template != null) { - String templateIdWithoutSuffix = pathToPathWithoutSuffix.get(template.getId()); - - final List parameterDeclarations; - if (checkedTemplateIdToParamDecl.isEmpty()) { - parameterDeclarations = template.getParameterDeclarations(); - } else { - // Add method parameter declarations if they were not overridden in the template - parameterDeclarations = mergeParamDeclarations( - template.getParameterDeclarations(), - checkedTemplateIdToParamDecl.get(templateIdWithoutSuffix)); - } + String templateIdWithoutSuffix = templatePathWithoutSuffix(template.getId(), config); if (!checkedFragments.isEmpty()) { for (CheckedTemplateBuildItem checkedFragment : checkedFragments) { @@ -766,21 +732,15 @@ public void beforeParsing(ParserHelper parserHelper) { } analysis.add(new TemplateAnalysis(null, template.getGeneratedId(), template.getExpressions(), - parameterDeclarations, path.getPath(), template.getFragmentIds())); + template.getParameterDeclarations(), path.getPath(), template.getFragmentIds())); } } // Message bundle templates for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) { Template template = dummyEngine.parse(messageBundleMethod.getTemplate(), null, messageBundleMethod.getTemplateId()); - - // Add method parameter declarations if they were not overridden in the template - List paramDeclarations = mergeParamDeclarations( - template.getParameterDeclarations(), - msgBundleTemplateIdToParamDecl.get(messageBundleMethod.getTemplateId())); - analysis.add(new TemplateAnalysis(messageBundleMethod.getTemplateId(), template.getGeneratedId(), - template.getExpressions(), paramDeclarations, + template.getExpressions(), template.getParameterDeclarations(), messageBundleMethod.getMethod().declaringClass().name() + "#" + messageBundleMethod.getMethod().name() + "()", template.getFragmentIds())); @@ -791,6 +751,17 @@ public void beforeParsing(ParserHelper parserHelper) { return new TemplatesAnalysisBuildItem(analysis); } + private String templatePathWithoutSuffix(String path, QuteConfig config) { + for (String suffix : config.suffixes) { + if (path.endsWith(suffix)) { + // Remove the suffix + path = path.substring(0, path.length() - (suffix.length() + 1)); + break; + } + } + return path; + } + @BuildStep void validateCheckedFragments(List validations, List expressionMatches, @@ -925,29 +896,6 @@ private static String getCheckedTemplateParameterParameterizedTypeName(Parameter return builder.toString(); } - private List mergeParamDeclarations(List parameterDeclarations, - Map paramNameToDeclaration) { - if (paramNameToDeclaration != null) { - Map mergeResult = new HashMap<>(paramNameToDeclaration); - for (ParameterDeclaration paramDeclaration : parameterDeclarations) { - // Template parameter declarations override method parameter declarations - mergeResult.put(paramDeclaration.getKey(), paramDeclaration); - } - return List.copyOf(mergeResult.values()); - } - return parameterDeclarations; - } - - private void addMethodParamsToParserHelper(ParserHelper parserHelper, String templateId, - Map> templateIdToParamDecl) { - var paramNameToDeclaration = templateIdToParamDecl.get(templateId); - if (paramNameToDeclaration != null) { - for (MethodParameterDeclaration parameterDeclaration : paramNameToDeclaration.values()) { - parserHelper.addParameter(parameterDeclaration.getKey(), parameterDeclaration.getParamType()); - } - } - } - @BuildStep void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, BeanArchiveIndexBuildItem beanArchiveIndex, @@ -1481,17 +1429,24 @@ private static NamespaceResult processNamespace(Expression expression, MatchResu // data: Expression.Part firstPart = expression.getParts().get(0); String firstPartName = firstPart.getName(); - for (ParameterDeclaration paramDeclaration : templateAnalysis.parameterDeclarations) { - if (paramDeclaration.getKey().equals(firstPartName)) { - // Data Namespace expression has bounded parameter declaration - dataNamespaceTypeInfo = TypeInfos - .create(paramDeclaration.getTypeInfo(), firstPart, index, templateIdToPathFun, - expression.getOrigin()) - .asTypeInfo(); + // FIXME This is not entirely correct + // First we try to find a non-synthetic param declaration that matches the given name, + // and then we try the synthetic ones. + // However, this might result in confusing behavior when type-safe templates are used together with type-safe expressions. + // But this should not be a common use case. + ParameterDeclaration paramDeclaration = null; + for (ParameterDeclaration pd : templateAnalysis.getSortedParameterDeclarations()) { + if (pd.getKey().equals(firstPartName)) { + paramDeclaration = pd; break; } } - if (dataNamespaceTypeInfo == null) { + if (paramDeclaration != null) { + dataNamespaceTypeInfo = TypeInfos + .create(paramDeclaration.getTypeInfo(), firstPart, index, templateIdToPathFun, + expression.getOrigin()) + .asTypeInfo(); + } else { putResult(match, results, expression); ignored = true; } @@ -3538,39 +3493,4 @@ public String getName() { } - private static final class MethodParameterDeclaration implements ParameterDeclaration { - - private final String paramType; - private final String paramName; - - private MethodParameterDeclaration(String paramType, String paramName) { - this.paramType = paramType; - this.paramName = paramName; - } - - public String getParamType() { - return paramType; - } - - @Override - public String getTypeInfo() { - return Expressions.typeInfoFrom(paramType); - } - - @Override - public String getKey() { - return paramName; - } - - @Override - public Expression getDefaultValue() { - return null; - } - - @Override - public TemplateNode.Origin getOrigin() { - return null; - } - } - } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java index 830588305c4eca..d67efdf86ab795 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java @@ -1,5 +1,7 @@ package io.quarkus.qute.deployment; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Set; @@ -62,6 +64,23 @@ Expression findExpression(int id) { return null; } + /** + * Non-synthetic declarations go first, then sorted by the line. + * + * @return the sorted list of parameter declarations + */ + public List getSortedParameterDeclarations() { + List ret = new ArrayList<>(parameterDeclarations); + ret.sort(new Comparator() { + @Override + public int compare(ParameterDeclaration pd1, ParameterDeclaration pd2) { + int ret = Boolean.compare(pd1.getOrigin().isSynthetic(), pd2.getOrigin().isSynthetic()); + return ret == 0 ? Integer.compare(pd1.getOrigin().getLine(), pd2.getOrigin().getLine()) : ret; + } + }); + return ret; + } + @Override public int hashCode() { final int prime = 31; diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateAnalysisTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateAnalysisTest.java new file mode 100644 index 00000000000000..143d778592b4fe --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateAnalysisTest.java @@ -0,0 +1,87 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import io.quarkus.qute.Expression; +import io.quarkus.qute.ParameterDeclaration; +import io.quarkus.qute.TemplateNode.Origin; +import io.quarkus.qute.Variant; +import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; + +public class TemplateAnalysisTest { + + @Test + public void testSortedParamDeclarations() { + TemplateAnalysis analysis = new TemplateAnalysis(null, null, null, List.of(paramDeclaration("foo", -1), + paramDeclaration("bar", -1), paramDeclaration("qux", 10), paramDeclaration("baz", 1)), null, null); + List sorted = analysis.getSortedParameterDeclarations(); + assertEquals(4, sorted.size()); + assertEquals("baz", sorted.get(0).getKey()); + assertEquals("qux", sorted.get(1).getKey()); + assertTrue(sorted.get(2).getKey().equals("foo") || sorted.get(2).getKey().equals("bar")); + assertTrue(sorted.get(3).getKey().equals("foo") || sorted.get(3).getKey().equals("bar")); + } + + ParameterDeclaration paramDeclaration(String key, int line) { + return new ParameterDeclaration() { + + @Override + public String getTypeInfo() { + return null; + } + + @Override + public Origin getOrigin() { + return new Origin() { + + @Override + public Optional getVariant() { + return Optional.empty(); + } + + @Override + public String getTemplateId() { + return null; + } + + @Override + public String getTemplateGeneratedId() { + return null; + } + + @Override + public int getLineCharacterStart() { + return 0; + } + + @Override + public int getLineCharacterEnd() { + return 0; + } + + @Override + public int getLine() { + return line; + } + }; + } + + @Override + public String getKey() { + return key; + } + + @Override + public Expression getDefaultValue() { + return null; + } + }; + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParameterDeclaration.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParameterDeclaration.java index 6a6da4bce112b5..8373f9f3cd1d8b 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParameterDeclaration.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParameterDeclaration.java @@ -4,7 +4,7 @@ import io.quarkus.qute.TemplateNode.Origin; /** - * Represents a parameter declaration, i.e. {@org.acme.Foo foo}. + * Represents a type parameter declaration, i.e. {@org.acme.Foo foo}. */ public interface ParameterDeclaration { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index dad999d56c2112..b6eba251f7d9bc 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -126,9 +126,19 @@ private Template currentTemplate() { Template parse() { - sectionStack.addFirst(SectionNode.builder(ROOT_HELPER_NAME, origin(0), this, this) + SectionNode.Builder rootBuilder = SectionNode.builder(ROOT_HELPER_NAME, syntheticOrigin(), this, this) .setEngine(engine) - .setHelperFactory(ROOT_SECTION_HELPER_FACTORY)); + .setHelperFactory(ROOT_SECTION_HELPER_FACTORY); + sectionStack.addFirst(rootBuilder); + + // Add synthetic nodes for param declarations added by parser hooks + Map bindings = scopeStack.peek().getBindings(); + if (bindings != null && !bindings.isEmpty()) { + for (Entry e : bindings.entrySet()) { + rootBuilder.currentBlock().addNode( + new ParameterDeclarationNode(e.getValue(), e.getKey(), null, syntheticOrigin())); + } + } long start = System.nanoTime(); Reader r = reader; @@ -1066,6 +1076,10 @@ Origin origin(int lineCharacterOffset) { return new OriginImpl(line, lineCharacter - lineCharacterOffset, lineCharacter, templateId, generatedId, variant); } + Origin syntheticOrigin() { + return new OriginImpl(-1, -1, -1, templateId, generatedId, variant); + } + private List> readLines(SectionNode rootNode) { List> lines = new ArrayList<>(); // Add the last line manually - there is no line separator to trigger flush diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java index 9d25886f270b27..d44fb05de3c85d 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java @@ -16,8 +16,7 @@ public interface ParserHelper { /** * Adds an implicit parameter declaration. This is an alternative approach to explicit parameter - * declarations - * used directly in the templates, e.g. {@org.acme.Foo foo}. + * declarations used directly in the templates, e.g. {@org.acme.Foo foo}. * * @param name * @param type diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java index b60e10c5691d65..3760d45e2bac7e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java @@ -38,6 +38,10 @@ public String getBinding(String binding) { return parentScope != null ? parentScope.getBinding(binding) : null; } + Map getBindings() { + return bindings; + } + public String getBindingTypeOrDefault(String binding, String defaultValue) { String type = getBinding(binding); return type != null ? type : defaultValue; diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java index 3f9008fa2f998b..615d72b3da5308 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java @@ -155,9 +155,12 @@ default String render() { Optional getVariant(); /** + * Returns all type parameter declarations of the template, including the declarations added by a + * {@link io.quarkus.qute.ParserHook}. + *

* If invoked upon a fragment instance then delegate to the defining template. * - * @return an immutable list of all parameter declarations defined in the template + * @return an immutable list of all type parameter declarations defined in the template */ List getParameterDeclarations(); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java index af05f6c74d283b..23096cb61698cf 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java @@ -107,6 +107,13 @@ default void appendTo(StringBuilder builder) { } } + /** + * @return {@code true} if the template node was not part of the original template, {@code false} otherwise + */ + default boolean isSynthetic() { + return getLine() == -1; + } + } } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java index 350b092e20513e..6337cadc49d36f 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -165,6 +166,10 @@ public void testLines() { assertEquals(8, itemNameOrigin.getLine()); assertEquals(1, itemNameOrigin.getLineCharacterStart()); assertEquals(11, itemNameOrigin.getLineCharacterEnd()); + ParameterDeclaration pd = template.getParameterDeclarations().get(0); + assertEquals(1, pd.getOrigin().getLine()); + assertEquals("foo", pd.getKey()); + assertNull(pd.getDefaultValue()); } @Test @@ -316,9 +321,16 @@ public void testParserHook() { public void beforeParsing(ParserHelper parserHelper) { parserHelper.addContentFilter(contents -> contents.replace("bard", "bar")); parserHelper.addContentFilter(contents -> contents.replace("${", "$\\{")); + parserHelper.addParameter("foo", String.class.getName()); } }).build().parse("${foo}::{bard}"); assertEquals("${foo}::true", template.data("bar", true).render()); + List paramDeclarations = template.getParameterDeclarations(); + assertEquals(1, paramDeclarations.size()); + ParameterDeclaration pd = paramDeclarations.get(0); + assertEquals("foo", pd.getKey()); + assertEquals("|java.lang.String|", pd.getTypeInfo()); + assertTrue(pd.getOrigin().isSynthetic()); } @Test From 09c105fe3730f1fffaa91e329c57a032154943d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Tue, 7 Nov 2023 15:28:41 +0100 Subject: [PATCH 06/68] Fix OIDC permission mapping NPE when scope is empty --- .../io/quarkus/oidc/runtime/OidcUtils.java | 7 ++++++ .../io/quarkus/it/keycloak/OidcResource.java | 18 +++++++++++++++ .../BearerTokenAuthorizationTest.java | 23 ++++++++++++++++--- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 7b3d41938b0b50..8fcc88f5c15ce4 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -208,6 +208,9 @@ private static List findClaimWithRoles(OidcTenantConfig.Roles rolesConfi return convertJsonArrayToList((JsonArray) claimValue); } else if (claimValue != null) { String sep = rolesConfig.getRoleClaimSeparator().isPresent() ? rolesConfig.getRoleClaimSeparator().get() : " "; + if (claimValue.toString().isBlank()) { + return Collections.emptyList(); + } return Arrays.asList(claimValue.toString().split(sep)); } else { return Collections.emptyList(); @@ -237,6 +240,10 @@ private static Object findClaimValue(String claimPath, JsonObject json, String[] private static List convertJsonArrayToList(JsonArray claimValue) { List list = new ArrayList<>(claimValue.size()); for (int i = 0; i < claimValue.size(); i++) { + String claimValueStr = claimValue.getString(i); + if (claimValueStr == null || claimValueStr.isBlank()) { + continue; + } list.add(claimValue.getString(i)); } return list; diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java index 7082b5ca3c6b0d..76f67a46ae8141 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java @@ -237,6 +237,16 @@ public String testAccessToken(@QueryParam("kid") String kid, @QueryParam("sub") " \"expires_in\": 300 }"; } + @POST + @Path("accesstoken-empty-scope") + @Produces("application/json") + public String testAccessTokenWithEmptyScope(@QueryParam("kid") String kid, @QueryParam("sub") String subject) { + return "{\"access_token\": \"" + jwt(null, subject, kid, true) + "\"," + + " \"token_type\": \"Bearer\"," + + " \"refresh_token\": \"123456789\"," + + " \"expires_in\": 300 }"; + } + @POST @Path("opaque-token") @Produces("application/json") @@ -290,6 +300,10 @@ public boolean disableRotate() { } private String jwt(String audience, String subject, String kid) { + return jwt(audience, subject, kid, false); + } + + private String jwt(String audience, String subject, String kid, boolean withEmptyScope) { JwtClaimsBuilder builder = Jwt.claim("typ", "Bearer") .upn("alice") .preferredUserName("alice") @@ -302,6 +316,10 @@ private String jwt(String audience, String subject, String kid) { builder.subject(subject); } + if (withEmptyScope) { + builder.claim("scope", ""); + } + return builder.jws().keyId(kid) .sign(key.getPrivateKey()); } diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index 73d7f7b646591e..12fe6f454dd4e9 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -517,15 +517,24 @@ public void testJwtTokenIntrospectionOnlyAndUserInfo() { + "introspection_client_id:none,introspection_client_secret:none,active:true,userinfo:alice,cache-size:0")); } + // verifies empty scope claim makes no difference (e.g. doesn't cause NPE) + RestAssured.given().auth().oauth2(getAccessTokenWithEmptyScopeFromSimpleOidc("2")) + .when().get("/tenant/tenant-oidc-introspection-only/api/user") + .then() + .statusCode(200) + .body(equalTo( + "tenant-oidc-introspection-only:alice,client_id:client-introspection-only," + + "introspection_client_id:none,introspection_client_secret:none,active:true,userinfo:alice,cache-size:0")); + RestAssured.given().auth().oauth2(getAccessTokenFromSimpleOidc("987654321", "2")) .when().get("/tenant/tenant-oidc-introspection-only/api/user") .then() .statusCode(401); RestAssured.when().get("/oidc/jwk-endpoint-call-count").then().body(equalTo("0")); - RestAssured.when().get("/oidc/introspection-endpoint-call-count").then().body(equalTo("4")); + RestAssured.when().get("/oidc/introspection-endpoint-call-count").then().body(equalTo("5")); RestAssured.when().post("/oidc/disable-introspection").then().body(equalTo("false")); - RestAssured.when().get("/oidc/userinfo-endpoint-call-count").then().body(equalTo("4")); + RestAssured.when().get("/oidc/userinfo-endpoint-call-count").then().body(equalTo("5")); RestAssured.when().get("/cache/size").then().body(equalTo("0")); } @@ -694,13 +703,21 @@ private String getAccessTokenFromSimpleOidc(String kid) { } private String getAccessTokenFromSimpleOidc(String subject, String kid) { + return getAccessTokenFromSimpleOidc(subject, kid, "/oidc/accesstoken"); + } + + private String getAccessTokenWithEmptyScopeFromSimpleOidc(String kid) { + return getAccessTokenFromSimpleOidc("123456789", kid, "/oidc/accesstoken-empty-scope"); + } + + private static String getAccessTokenFromSimpleOidc(String subject, String kid, String tokenEndpoint) { String json = RestAssured .given() .queryParam("sub", subject) .queryParam("kid", kid) .formParam("grant_type", "authorization_code") .when() - .post("/oidc/accesstoken") + .post(tokenEndpoint) .body().asString(); JsonObject object = new JsonObject(json); return object.getString("access_token"); From b7ae1aca1e4f2b4d7f1dcc65637bd671c00eea1d Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 7 Nov 2023 18:02:07 +0100 Subject: [PATCH 07/68] Add keywords and topics for hibernate-search-orm-elasticsearch.adoc Will be consumed by the search service. --- docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc b/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc index f0e999d807b233..37798c850598cd 100644 --- a/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc +++ b/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc @@ -7,7 +7,8 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Hibernate Search allows you to index your entities in an Elasticsearch cluster and easily offer full text search in all your Hibernate ORM-based applications. -:topics: data,hibernate-search,elasticsearch,search,nosql +:keywords: elasticsearch hibernate orm search +:topics: data,hibernate-search,elasticsearch,search,nosql,hibernate-orm :extensions: io.quarkus:quarkus-hibernate-search-orm-elasticsearch You have a Hibernate ORM-based application? You want to provide a full-featured full-text search to your users? You're at the right place. From 8833e356b50798c8de11e80afb72ab62de9a2e97 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 7 Nov 2023 18:36:28 +0100 Subject: [PATCH 08/68] Make Dependabot group micro updates --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 293c230e2cd9cf..983b47e2ab8bae 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,10 @@ updates: open-pull-requests-limit: 6 labels: - area/dependencies + groups: + patches: + update-types: + - "patch" allow: - dependency-name: org.jboss:jboss-parent - dependency-name: org.jboss.resteasy:* From d89abdbc9c36a1e8da206eba8a9be01558bbdc5c Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Tue, 7 Nov 2023 16:07:50 -0500 Subject: [PATCH 09/68] Fix grammar error: Replace "different to" with "different from". Co-Authored-By: Rolfe Dlugy-Hegwer --- .../quarkus/oidc/common/runtime/OidcCommonConfig.java | 11 +++++------ .../io/quarkus/bootstrap/app/QuarkusBootstrap.java | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index 1d2692ff68939a..46527078111bae 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -61,13 +61,12 @@ public class OidcCommonConfig { public Optional connectionDelay = Optional.empty(); /** - * The number of times an attempt to re-establish an already available connection will be repeated for. - * Note this property is different to the `connection-delay` property which is only effective during the initial OIDC + * The number of times an attempt to re-establish an already available connection will be repeated. + * Note this property is different from the `connection-delay` property, which is only effective during the initial OIDC * connection creation. - * This property is used to try to recover the existing connection which may have been temporarily lost. - * For example, if a request to the OIDC token endpoint fails due to a connection exception then the request will be retried - * for - * a number of times configured by this property. + * This property is used to try to recover an existing connection that may have been temporarily lost. + * For example, if a request to the OIDC token endpoint fails due to a connection exception, then the request will be + * retried the number of times configured by this property. */ @ConfigItem(defaultValue = "3") public int connectionRetryCount = 3; diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java index ea8d53f0c959a0..70ba824e22dce7 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java @@ -39,7 +39,7 @@ public class QuarkusBootstrap implements Serializable { private final PathCollection applicationRoot; /** - * The root of the project. This may be different to the application root for tests that + * The root of the project. This may be different from the application root for tests that * run in a different directory. */ private final Path projectRoot; From f647a5b86ace2a09f0f30a872ab90f3faddc14c9 Mon Sep 17 00:00:00 2001 From: David Cotton Date: Sat, 4 Nov 2023 17:28:30 +0100 Subject: [PATCH 10/68] Adds Pattern support to Kafka Streams Topology Dev UI --- .../devui/KafkaStreamsJsonRPCService.java | 8 +- .../runtime/devui/TopologyParserContext.java | 32 +++-- .../devui/KafkaStreamsJsonRPCServiceTest.java | 112 ++++++++++-------- 3 files changed, 94 insertions(+), 58 deletions(-) diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java index fc82604f416587..24f25e424e5c2c 100644 --- a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java @@ -66,7 +66,7 @@ public void accept(TopologyParserContext context) { private static final RawTopologyItemParser SOURCE = new RawTopologyItemParser() { private final Pattern sourcePattern = Pattern - .compile("Source:\\s+(?\\S+)\\s+\\(topics:\\s+\\[(?.*)\\]\\).*"); + .compile("Source:\\s+(?\\S+)\\s+\\(topics:\\s+((\\[(?.*)\\])|(?.*)\\)).*"); private Matcher matcher; @Override @@ -77,7 +77,11 @@ public boolean test(String line) { @Override public void accept(TopologyParserContext context) { - context.addSources(matcher.group("source"), matcher.group("topics").split(",")); + if (matcher.group("topics") != null) { + context.addSources(matcher.group("source"), matcher.group("topics").split(",")); + } else if (matcher.group("regex") != null) { + context.addRegexSource(matcher.group("source"), matcher.group("regex")); + } } }; diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java index cdbf1ec7c1a51d..6a047065af724a 100644 --- a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java @@ -41,6 +41,16 @@ void addSources(String source, String[] topics) { }); } + void addRegexSource(String source, String regex) { + currentNode = source; + final var cleanRegex = regex.trim(); + if (!cleanRegex.isEmpty()) { + sources.add(cleanRegex); + graphviz.addRegexSource(source, cleanRegex); + mermaid.addRegexSource(source, cleanRegex); + } + } + void addStores(String[] stores, String processor, boolean join) { currentNode = processor; Arrays.stream(stores) @@ -71,8 +81,8 @@ String toGraph() { final var res = new ArrayList(); res.add("digraph {"); - res.add(" fontname=\"Helvetica\"; fontsize=\"10\";"); - res.add(" node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=\"Helvetica\" fontsize=\"10\"];"); + res.add(" fontname=Helvetica; fontsize=10;"); + res.add(" node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=Helvetica fontsize=10];"); nodes.forEach(n -> res.add(' ' + n + ';')); subGraphs.entrySet().forEach(e -> { res.add(" subgraph cluster" + e.getKey() + " {"); @@ -80,11 +90,6 @@ String toGraph() { e.getValue().forEach(v -> res.add(" " + v + ';')); res.add(" }"); }); - for (int i = 0; i < subGraphs.size(); i++) { - res.add(" subgraph cluster" + i + " {"); - res.add(" label=\"Sub-Topology: " + i + "\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";"); - res.add(" }"); - } edges.forEach(e -> res.add(' ' + e + ';')); res.add("}"); @@ -108,6 +113,15 @@ private void addSource(String source, String topic) { subGraphs.get(currentGraph).add(toId(source)); } + private void addRegexSource(String source, String regex) { + final var regexId = "REGEX_" + nodes.size(); + final var regexLabel = regex.replaceAll("\\\\", "\\\\\\\\"); + nodes.add(regexId + " [label=\"" + regexLabel + "\" shape=invhouse style=dashed margin=\"0,0\"]"); + nodes.add(toId(source) + " [label=\"" + toLabel(source) + "\"]"); + edges.add(regexId + " -> " + toId(source)); + subGraphs.get(currentGraph).add(toId(source)); + } + private void addTarget(String target, String node) { nodes.add(toId(target) + " [label=\"" + toLabel(target) + "\"]"); edges.add(toId(node) + " -> " + toId(target)); @@ -164,6 +178,10 @@ private void addSource(String source, String topic) { endpoints.add(topic + '[' + topic + "] --> " + source + '(' + toName(source) + ')'); } + private void addRegexSource(String source, String regex) { + endpoints.add("REGEX_" + endpoints.size() + '[' + regex + "] --> " + source + '(' + toName(source) + ')'); + } + private void addTarget(String target, String node) { subTopologies.add(' ' + node + '[' + toName(node) + "] --> " + target + '(' + toName(target) + ')'); } diff --git a/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java b/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java index 8c40491616d87e..854ac92d972512 100644 --- a/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java +++ b/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java @@ -31,66 +31,77 @@ public void shouldParsingStayConstant() { + " --> KSTREAM-SINK-0000000007\n" + " <-- KSTREAM-AGGREGATE-0000000005\n" + " Sink: KSTREAM-SINK-0000000007 (topic: temperatures-aggregated)\n" - + " <-- KTABLE-TOSTREAM-0000000006"; + + " <-- KTABLE-TOSTREAM-0000000006\n" + + "\n" + + " Sub-topology: 2\n" + + " Source: KSTREAM-SOURCE-0000000008 (topics: notification\\..+)\n" + + " --> KSTREAM-FOREACH-0000000009\n" + + " Processor: KSTREAM-FOREACH-0000000009 (stores: [])\n" + + " --> none\n" + + " <-- KSTREAM-SOURCE-0000000008"; final var actual = rpcService.parseTopologyDescription(expectedDescribe); assertEquals(expectedDescribe, actual.getString("describe")); - assertEquals("[0, 1]", actual.getString("subTopologies")); - assertEquals("[temperature-values, weather-stations]", actual.getString("sources")); + assertEquals("[0, 1, 2]", actual.getString("subTopologies")); + assertEquals("[notification\\..+, temperature-values, weather-stations]", actual.getString("sources")); assertEquals("[temperatures-aggregated]", actual.getString("sinks")); assertEquals("[weather-stations-STATE-STORE-0000000000, weather-stations-store]", actual.getString("stores")); - assertEquals("digraph {\n" + - " fontname=\"Helvetica\"; fontsize=\"10\";\n" + - " node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=\"Helvetica\" fontsize=\"10\"];\n" + - " weather_stations [label=\"weather\\nstations\" shape=invhouse margin=\"0,0\"];\n" + - " KSTREAM_SOURCE_0000000001 [label=\"KSTREAM\\nSOURCE\\n0000000001\"];\n" + - " KTABLE_SOURCE_0000000002 [label=\"KTABLE\\nSOURCE\\n0000000002\"];\n" + - " weather_stations_STATE_STORE_0000000000 [label=\"weather\\nstations\\nSTATE\\nSTORE\\n0000000000\" shape=cylinder];\n" - + - " temperature_values [label=\"temperature\\nvalues\" shape=invhouse margin=\"0,0\"];\n" + - " KSTREAM_SOURCE_0000000003 [label=\"KSTREAM\\nSOURCE\\n0000000003\"];\n" + - " KSTREAM_LEFTJOIN_0000000004 [label=\"KSTREAM\\nLEFTJOIN\\n0000000004\"];\n" + - " KSTREAM_AGGREGATE_0000000005 [label=\"KSTREAM\\nAGGREGATE\\n0000000005\"];\n" + - " weather_stations_store [label=\"weather\\nstations\\nstore\" shape=cylinder];\n" + - " KTABLE_TOSTREAM_0000000006 [label=\"KTABLE\\nTOSTREAM\\n0000000006\"];\n" + - " KSTREAM_SINK_0000000007 [label=\"KSTREAM\\nSINK\\n0000000007\"];\n" + - " temperatures_aggregated [label=\"temperatures\\naggregated\" shape=house margin=\"0,0\"];\n" + - " subgraph cluster0 {\n" + - " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " KSTREAM_SOURCE_0000000001;\n" + - " KTABLE_SOURCE_0000000002;\n" + - " }\n" + - " subgraph cluster1 {\n" + - " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " KSTREAM_SOURCE_0000000003;\n" + - " KSTREAM_LEFTJOIN_0000000004;\n" + - " KSTREAM_AGGREGATE_0000000005;\n" + - " KTABLE_TOSTREAM_0000000006;\n" + - " KSTREAM_SINK_0000000007;\n" + - " }\n" + - " subgraph cluster0 {\n" + - " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " }\n" + - " subgraph cluster1 {\n" + - " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " }\n" + - " weather_stations -> KSTREAM_SOURCE_0000000001;\n" + - " KSTREAM_SOURCE_0000000001 -> KTABLE_SOURCE_0000000002;\n" + - " KTABLE_SOURCE_0000000002 -> weather_stations_STATE_STORE_0000000000;\n" + - " temperature_values -> KSTREAM_SOURCE_0000000003;\n" + - " KSTREAM_SOURCE_0000000003 -> KSTREAM_LEFTJOIN_0000000004;\n" + - " KSTREAM_LEFTJOIN_0000000004 -> KSTREAM_AGGREGATE_0000000005;\n" + - " KSTREAM_AGGREGATE_0000000005 -> weather_stations_store;\n" + - " KSTREAM_AGGREGATE_0000000005 -> KTABLE_TOSTREAM_0000000006;\n" + - " KTABLE_TOSTREAM_0000000006 -> KSTREAM_SINK_0000000007;\n" + - " KSTREAM_SINK_0000000007 -> temperatures_aggregated;\n" + - "}", actual.getString("graphviz")); + assertEquals("digraph {\n" + + " fontname=Helvetica; fontsize=10;\n" + + " node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=Helvetica fontsize=10];\n" + + " weather_stations [label=\"weather\\nstations\" shape=invhouse margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000001 [label=\"KSTREAM\\nSOURCE\\n0000000001\"];\n" + + " KTABLE_SOURCE_0000000002 [label=\"KTABLE\\nSOURCE\\n0000000002\"];\n" + + " weather_stations_STATE_STORE_0000000000 [label=\"weather\\nstations\\nSTATE\\nSTORE\\n0000000000\" shape=cylinder];\n" + + " temperature_values [label=\"temperature\\nvalues\" shape=invhouse margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000003 [label=\"KSTREAM\\nSOURCE\\n0000000003\"];\n" + + " KSTREAM_LEFTJOIN_0000000004 [label=\"KSTREAM\\nLEFTJOIN\\n0000000004\"];\n" + + " KSTREAM_AGGREGATE_0000000005 [label=\"KSTREAM\\nAGGREGATE\\n0000000005\"];\n" + + " weather_stations_store [label=\"weather\\nstations\\nstore\" shape=cylinder];\n" + + " KTABLE_TOSTREAM_0000000006 [label=\"KTABLE\\nTOSTREAM\\n0000000006\"];\n" + + " KSTREAM_SINK_0000000007 [label=\"KSTREAM\\nSINK\\n0000000007\"];\n" + + " temperatures_aggregated [label=\"temperatures\\naggregated\" shape=house margin=\"0,0\"];\n" + + " REGEX_12 [label=\"notification\\\\..+\" shape=invhouse style=dashed margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000008 [label=\"KSTREAM\\nSOURCE\\n0000000008\"];\n" + + " KSTREAM_FOREACH_0000000009 [label=\"KSTREAM\\nFOREACH\\n0000000009\"];\n" + + " subgraph cluster0 {\n" + + " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000001;\n" + + " KTABLE_SOURCE_0000000002;\n" + + " }\n" + + " subgraph cluster1 {\n" + + " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000003;\n" + + " KSTREAM_LEFTJOIN_0000000004;\n" + + " KSTREAM_AGGREGATE_0000000005;\n" + + " KTABLE_TOSTREAM_0000000006;\n" + + " KSTREAM_SINK_0000000007;\n" + + " }\n" + + " subgraph cluster2 {\n" + + " label=\"Sub-Topology: 2\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000008;\n" + + " KSTREAM_FOREACH_0000000009;\n" + + " }\n" + + " weather_stations -> KSTREAM_SOURCE_0000000001;\n" + + " KSTREAM_SOURCE_0000000001 -> KTABLE_SOURCE_0000000002;\n" + + " KTABLE_SOURCE_0000000002 -> weather_stations_STATE_STORE_0000000000;\n" + + " temperature_values -> KSTREAM_SOURCE_0000000003;\n" + + " KSTREAM_SOURCE_0000000003 -> KSTREAM_LEFTJOIN_0000000004;\n" + + " KSTREAM_LEFTJOIN_0000000004 -> KSTREAM_AGGREGATE_0000000005;\n" + + " KSTREAM_AGGREGATE_0000000005 -> weather_stations_store;\n" + + " KSTREAM_AGGREGATE_0000000005 -> KTABLE_TOSTREAM_0000000006;\n" + + " KTABLE_TOSTREAM_0000000006 -> KSTREAM_SINK_0000000007;\n" + + " KSTREAM_SINK_0000000007 -> temperatures_aggregated;\n" + + " REGEX_12 -> KSTREAM_SOURCE_0000000008;\n" + + " KSTREAM_SOURCE_0000000008 -> KSTREAM_FOREACH_0000000009;\n" + + "}", actual.getString("graphviz")); assertEquals("graph TD\n" + " weather-stations[weather-stations] --> KSTREAM-SOURCE-0000000001(KSTREAM-
SOURCE-
0000000001)\n" + " KTABLE-SOURCE-0000000002[KTABLE-
SOURCE-
0000000002] --> weather-stations-STATE-STORE-0000000000(weather-
stations-
STATE-
STORE-
0000000000)\n" + " temperature-values[temperature-values] --> KSTREAM-SOURCE-0000000003(KSTREAM-
SOURCE-
0000000003)\n" + " KSTREAM-AGGREGATE-0000000005[KSTREAM-
AGGREGATE-
0000000005] --> weather-stations-store(weather-
stations-
store)\n" + " KSTREAM-SINK-0000000007[KSTREAM-
SINK-
0000000007] --> temperatures-aggregated(temperatures-aggregated)\n" + + " REGEX_5[notification\\..+] --> KSTREAM-SOURCE-0000000008(KSTREAM-
SOURCE-
0000000008)\n" + " subgraph Sub-Topology: 0\n" + " KSTREAM-SOURCE-0000000001[KSTREAM-
SOURCE-
0000000001] --> KTABLE-SOURCE-0000000002(KTABLE-
SOURCE-
0000000002)\n" + " end\n" @@ -99,6 +110,9 @@ public void shouldParsingStayConstant() { + " KSTREAM-LEFTJOIN-0000000004[KSTREAM-
LEFTJOIN-
0000000004] --> KSTREAM-AGGREGATE-0000000005(KSTREAM-
AGGREGATE-
0000000005)\n" + " KSTREAM-AGGREGATE-0000000005[KSTREAM-
AGGREGATE-
0000000005] --> KTABLE-TOSTREAM-0000000006(KTABLE-
TOSTREAM-
0000000006)\n" + " KTABLE-TOSTREAM-0000000006[KTABLE-
TOSTREAM-
0000000006] --> KSTREAM-SINK-0000000007(KSTREAM-
SINK-
0000000007)\n" + + " end\n" + + " subgraph Sub-Topology: 2\n" + + " KSTREAM-SOURCE-0000000008[KSTREAM-
SOURCE-
0000000008] --> KSTREAM-FOREACH-0000000009(KSTREAM-
FOREACH-
0000000009)\n" + " end", actual.getString("mermaid")); } } From 88db04697a25079d4b6089b5cf090850aa5b9b60 Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Tue, 7 Nov 2023 12:22:42 +0100 Subject: [PATCH 11/68] Support byte array arguments for Redis commands in the datasource API --- .../RedisCommandExtraArguments.java | 8 +- .../io/quarkus/redis/datasource/SortArgs.java | 4 +- .../redis/datasource/autosuggest/GetArgs.java | 4 +- .../redis/datasource/bitmap/BitFieldArgs.java | 4 +- .../redis/datasource/bloom/BfInsertArgs.java | 4 +- .../redis/datasource/bloom/BfReserveArgs.java | 4 +- .../redis/datasource/cuckoo/CfInsertArgs.java | 4 +- .../datasource/cuckoo/CfReserveArgs.java | 4 +- .../redis/datasource/geo/GeoAddArgs.java | 4 +- .../redis/datasource/geo/GeoRadiusArgs.java | 4 +- .../datasource/geo/GeoRadiusStoreArgs.java | 4 +- .../redis/datasource/geo/GeoSearchArgs.java | 6 +- .../datasource/geo/GeoSearchStoreArgs.java | 4 +- .../redis/datasource/json/JsonSetArgs.java | 4 +- .../redis/datasource/keys/CopyArgs.java | 4 +- .../redis/datasource/keys/ExpireArgs.java | 4 +- .../redis/datasource/list/LPosArgs.java | 4 +- .../datasource/search/AggregateArgs.java | 4 +- .../redis/datasource/search/CreateArgs.java | 4 +- .../datasource/search/HighlightArgs.java | 4 +- .../redis/datasource/search/IndexedField.java | 4 +- .../redis/datasource/search/QueryArgs.java | 95 ++++++++++++++++++- .../datasource/search/SpellCheckArgs.java | 4 +- .../datasource/search/SummarizeArgs.java | 4 +- .../redis/datasource/sortedset/ZAddArgs.java | 6 +- .../datasource/sortedset/ZAggregateArgs.java | 4 +- .../redis/datasource/sortedset/ZMpopArgs.java | 4 +- .../datasource/sortedset/ZRangeArgs.java | 4 +- .../redis/datasource/stream/StreamRange.java | 4 +- .../redis/datasource/stream/XAddArgs.java | 4 +- .../redis/datasource/stream/XClaimArgs.java | 4 +- .../datasource/stream/XGroupCreateArgs.java | 4 +- .../datasource/stream/XGroupSetIdArgs.java | 4 +- .../redis/datasource/stream/XPendingArgs.java | 4 +- .../redis/datasource/stream/XReadArgs.java | 4 +- .../datasource/stream/XReadGroupArgs.java | 4 +- .../redis/datasource/stream/XTrimArgs.java | 4 +- .../redis/datasource/string/GetExArgs.java | 4 +- .../redis/datasource/string/SetArgs.java | 4 +- .../redis/datasource/timeseries/AddArgs.java | 4 +- .../datasource/timeseries/AlterArgs.java | 4 +- .../datasource/timeseries/CreateArgs.java | 4 +- .../datasource/timeseries/IncrementArgs.java | 4 +- .../redis/datasource/timeseries/MGetArgs.java | 4 +- .../datasource/timeseries/MRangeArgs.java | 4 +- .../datasource/timeseries/RangeArgs.java | 4 +- .../redis/datasource/value/GetExArgs.java | 4 +- .../redis/datasource/value/SetArgs.java | 4 +- .../redis/runtime/datasource/Validation.java | 50 ++++++++++ .../redis/datasource/SearchCommandsTest.java | 70 ++++++++++++++ 50 files changed, 309 insertions(+), 102 deletions(-) diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java index 3d58d049a4dc37..9a76064c18f83e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java @@ -8,17 +8,17 @@ public interface RedisCommandExtraArguments { /** - * @return the list of arguments, encoded as a list of String. + * @return the list of arguments. */ - default List toArgs() { + default List toArgs() { return toArgs(null); } /** * @param encoder an optional encoder to encode some of the values - * @return the list of arguments, encoded as a list of String. + * @return the list of arguments. */ - default List toArgs(Codec encoder) { + default List toArgs(Codec encoder) { return Collections.emptyList(); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/SortArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/SortArgs.java index 840366dd0b463a..cc61d4f32be2ea 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/SortArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/SortArgs.java @@ -131,8 +131,8 @@ public SortArgs get(String get) { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (by != null && !by.isBlank()) { args.add("BY"); args.add(by); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/autosuggest/GetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/autosuggest/GetArgs.java index bbfeab15e1406f..4eb4fdd6345b0d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/autosuggest/GetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/autosuggest/GetArgs.java @@ -47,8 +47,8 @@ public GetArgs withScores() { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (fuzzy) { list.add("FUZZY"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bitmap/BitFieldArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bitmap/BitFieldArgs.java index 8137802e1fa826..4fa49b16200bf6 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bitmap/BitFieldArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bitmap/BitFieldArgs.java @@ -88,7 +88,7 @@ public String toString() { } - private final List commands = new ArrayList<>(); + private final List commands = new ArrayList<>(); private BitFieldType previousBitFieldType; /** @@ -351,7 +351,7 @@ private BitFieldType getPreviousFieldType() { } } - public List toArgs() { + public List toArgs() { return commands; } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java index 47c65d954c8daa..95cdd00ca4e699 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java @@ -82,8 +82,8 @@ public BfInsertArgs expansion(int expansion) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (capacity > 0) { list.add("CAPACITY"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfReserveArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfReserveArgs.java index 16df8a2b402672..ff8b0e3905c88c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfReserveArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfReserveArgs.java @@ -41,8 +41,8 @@ public BfReserveArgs expansion(int expansion) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (expansion > 0) { list.add("EXPANSION"); list.add(Integer.toString(expansion)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfInsertArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfInsertArgs.java index 2470caabc36e95..e1d9ae7b1b0b0e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfInsertArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfInsertArgs.java @@ -36,8 +36,8 @@ public CfInsertArgs nocreate() { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (capacity > 0) { list.add("CAPACITY"); list.add(Long.toString(capacity)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfReserveArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfReserveArgs.java index 5765a3615447d6..4310b59b26fcbb 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfReserveArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfReserveArgs.java @@ -51,8 +51,8 @@ public CfReserveArgs expansion(int expansion) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (bucketSize > 0) { list.add("BUCKETSIZE"); list.add(Long.toString(bucketSize)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoAddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoAddArgs.java index 5e05387ed47e7f..e54eb16166499d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoAddArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoAddArgs.java @@ -42,11 +42,11 @@ public GeoAddArgs ch() { return this; } - public List toArgs() { + public List toArgs() { if (xx && nx) { throw new IllegalArgumentException("Cannot set XX and NX together"); } - List args = new ArrayList<>(); + List args = new ArrayList<>(); if (xx) { args.add("XX"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusArgs.java index b8aa8263dea52d..3746e11df44e1d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusArgs.java @@ -101,13 +101,13 @@ public GeoRadiusArgs any() { } @Override - public List toArgs() { + public List toArgs() { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (withDistance) { list.add("WITHDIST"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java index e1e5b61a6328f3..5320e00c3f2ac8 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java @@ -127,7 +127,7 @@ public GeoRadiusStoreArgs storeDistKey(K storeDistKey) { } @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -136,7 +136,7 @@ public List toArgs(Codec codec) { throw new IllegalArgumentException("At least `STORE` or `STOREDIST` must be set"); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (withDistance) { list.add("WITHDIST"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java index 8d0c9f851c1211..9a41d13b8f6012 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java @@ -184,7 +184,7 @@ public GeoSearchArgs any() { } @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -198,7 +198,7 @@ public List toArgs(Codec codec) { throw new IllegalArgumentException("FROMMEMBER and FROMLONLAT cannot be used together"); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (member != null) { list.add("FROMMEMBER"); list.add(new String(codec.encode(member), StandardCharsets.UTF_8)); @@ -249,7 +249,7 @@ public boolean hasCoordinates() { return withCoordinates; } - private void putFlag(List list, boolean flag, String value) { + private void putFlag(List list, boolean flag, String value) { if (flag) { list.add(value); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java index fe9057eff57f88..550fd8b5db5553 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java @@ -146,7 +146,7 @@ public GeoSearchStoreArgs any() { } @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -160,7 +160,7 @@ public List toArgs(Codec codec) { throw new IllegalArgumentException("FROMMEMBER and FROMLONLAT cannot be used together"); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (member != null) { list.add("FROMMEMBER"); list.add(new String(codec.encode(member), StandardCharsets.UTF_8)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java index cb5f37c0dd8db8..8f88f3f947f227 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java @@ -31,11 +31,11 @@ public JsonSetArgs xx() { } @Override - public List toArgs() { + public List toArgs() { if (xx && nx) { throw new IllegalArgumentException("Cannot set XX and NX together"); } - List args = new ArrayList<>(); + List args = new ArrayList<>(); if (xx) { args.add("XX"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java index fb21c77d6ef0aa..155d2dfd1ae06f 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java @@ -37,8 +37,8 @@ public CopyArgs replace(boolean replace) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (destinationDb != -1) { args.add("DB"); args.add(Long.toString(destinationDb)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/ExpireArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/ExpireArgs.java index 36491aaff5f89a..30284cced7cbbb 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/ExpireArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/ExpireArgs.java @@ -55,8 +55,8 @@ public ExpireArgs gt() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); boolean exclusion = false; if (nx) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/list/LPosArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/list/LPosArgs.java index 4958815119bd0f..ad10fd8aefb410 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/list/LPosArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/list/LPosArgs.java @@ -40,8 +40,8 @@ public LPosArgs maxlen(long max) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (rank != 0) { list.add("RANK"); list.add(Long.toString(rank)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/AggregateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/AggregateArgs.java index dfb7a055a0a54f..51aa61b154ba9e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/AggregateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/AggregateArgs.java @@ -242,8 +242,8 @@ public AggregateArgs cursorMaxIdleTime(Duration maxIdleDuration) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (verbatim) { list.add("VERBATIM"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/CreateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/CreateArgs.java index 619944032d1e5b..c4910e7ddec713 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/CreateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/CreateArgs.java @@ -288,8 +288,8 @@ public CreateArgs indexedField(String field, String alias, FieldType type) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (onHash) { if (onJson) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/HighlightArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/HighlightArgs.java index 40dc79dca260c7..a9441b9a067544 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/HighlightArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/HighlightArgs.java @@ -49,8 +49,8 @@ public HighlightArgs tags(String open, String close) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); list.add("HIGHLIGHT"); if (fields != null && fields.length > 0) { list.add("FIELDS"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/IndexedField.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/IndexedField.java index 7b1f6cb63d7ffe..fc762b3fea51c8 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/IndexedField.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/IndexedField.java @@ -40,8 +40,8 @@ public static IndexedField from(String field, String alias, FieldType type) { this.options = options; } - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); list.add(field); if (alias != null) { list.add("AS"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java index 76d229440d0014..48b425bd3b4e8c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java @@ -8,6 +8,8 @@ import static io.smallrye.mutiny.helpers.ParameterValidation.positiveOrZero; import static io.smallrye.mutiny.helpers.ParameterValidation.validate; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -53,6 +55,7 @@ public class QueryArgs implements RedisCommandExtraArguments { private int count = -1; private Duration timeout; private final Map params = new HashMap<>(); + private final Map byteArrayParams = new HashMap<>(); private int dialect = -1; /** @@ -359,6 +362,61 @@ public QueryArgs param(String name, String value) { return this; } + /** + * Defines a parameter with a byte array value. + * @param name the parameter name + * @param value the parameter value as byte array + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, byte[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), notNullOrEmpty(value, "value")); + return this; + } + + /** + * Defines a parameter with a float array value. + * @param name the parameter name + * @param value the parameter value as array of floats + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, float[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), toByteArray(notNullOrEmpty(value, "value"))); + return this; + } + + /** + * Defines a parameter with a double array value. + * @param name the parameter name + * @param value the parameter value as array of doubles + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, double[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), toByteArray(notNullOrEmpty(value, "value"))); + return this; + } + + /** + * Defines a parameter with an int array value. + * @param name the parameter name + * @param value the parameter value as array of ints + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, int[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), toByteArray(notNullOrEmpty(value, "value"))); + return this; + } + + /** + * Defines a parameter with a long array value. + * @param name the parameter name + * @param value the parameter value as array of longs + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, long[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), toByteArray(notNullOrEmpty(value, "value"))); + return this; + } + /** * Selects the dialect version under which to execute the query. * If not specified, the query will execute under the default dialect version set during module initial loading. @@ -372,8 +430,8 @@ public QueryArgs dialect(int version) { } @Override - public List toArgs(Codec encoder) { - List list = new ArrayList<>(); + public List toArgs(Codec encoder) { + List list = new ArrayList<>(); if (nocontent) { list.add("NOCONTENT"); @@ -480,9 +538,13 @@ public List toArgs(Codec encoder) { list.add(Long.toString(timeout.toMillis())); } - if (!params.isEmpty()) { + if (!params.isEmpty() || !byteArrayParams.isEmpty()) { list.add("PARAMS"); - list.add(Integer.toString(params.size())); + list.add(Integer.toString(params.size() + byteArrayParams.size())); + for (Map.Entry entry : byteArrayParams.entrySet()) { + list.add(entry.getKey()); + list.add(entry.getValue()); + } for (Map.Entry entry : params.entrySet()) { list.add(entry.getKey()); list.add(entry.getValue()); @@ -527,4 +589,29 @@ public boolean containsPayload() { public boolean containsSortKeys() { return withSortKeys; } + + private byte[] toByteArray(float[] input) { + byte[] bytes = new byte[Float.BYTES * input.length]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(input); + return bytes; + } + + private byte[] toByteArray(double[] input) { + byte[] bytes = new byte[Double.BYTES * input.length]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asDoubleBuffer().put(input); + return bytes; + } + + private byte[] toByteArray(int[] input) { + byte[] bytes = new byte[Integer.BYTES * input.length]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().put(input); + return bytes; + } + + private byte[] toByteArray(long[] input) { + byte[] bytes = new byte[Long.BYTES * input.length]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asLongBuffer().put(input); + return bytes; + } + } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SpellCheckArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SpellCheckArgs.java index 9eaba0373fa692..2f35553d347c8e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SpellCheckArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SpellCheckArgs.java @@ -68,8 +68,8 @@ public SpellCheckArgs dialect(int dialect) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (distance != 0) { list.add("DISTANCE"); list.add(Integer.toString(distance)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SummarizeArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SummarizeArgs.java index d2630e606ed4d0..4d309145093f4b 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SummarizeArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SummarizeArgs.java @@ -75,8 +75,8 @@ public SummarizeArgs separator(String separator) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); list.add("SUMMARIZE"); if (fields != null && fields.length > 0) { list.add("FIELDS"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAddArgs.java index eb51d95ae732fa..eedcc0d66f85a1 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAddArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAddArgs.java @@ -69,7 +69,7 @@ public ZAddArgs gt() { } @Override - public List toArgs() { + public List toArgs() { if (xx && nx) { throw new IllegalArgumentException("Cannot use XX and NX together"); } @@ -77,7 +77,7 @@ public List toArgs() { throw new IllegalArgumentException("Cannot use LT and GT together"); } - List args = new ArrayList<>(); + List args = new ArrayList<>(); putFlag(args, nx, "NX"); putFlag(args, xx, "XX"); putFlag(args, lt, "LT"); @@ -86,7 +86,7 @@ public List toArgs() { return args; } - public void putFlag(List args, boolean value, String flag) { + public void putFlag(List args, boolean value, String flag) { if (value) { args.add(flag); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAggregateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAggregateArgs.java index 4a151a5568411a..1f12ec6c8bf3fa 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAggregateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAggregateArgs.java @@ -80,8 +80,8 @@ public ZAggregateArgs max() { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (!weights.isEmpty()) { args.add("WEIGHTS"); for (double w : weights) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZMpopArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZMpopArgs.java index 887cfa7a766c7f..5878b25ae02219 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZMpopArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZMpopArgs.java @@ -45,12 +45,12 @@ public ZMpopArgs count(int count) { } @Override - public List toArgs() { + public List toArgs() { if (min && max) { throw new IllegalArgumentException("Cannot use MIN and MAX together"); } - List args = new ArrayList<>(); + List args = new ArrayList<>(); if (min) { args.add("MIN"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZRangeArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZRangeArgs.java index 3102660a70ddb7..8a60c3d695090f 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZRangeArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZRangeArgs.java @@ -37,8 +37,8 @@ public ZRangeArgs limit(long offset, int count) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (rev) { list.add("REV"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java index 1bf69911cef026..9958a008eb6704 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java @@ -25,8 +25,8 @@ public static StreamRange of(String lowerBound, String higherBound) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); list.add(lowerBound); list.add(higherBound); return list; diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java index 196ebb8a091440..76c61ef5665079 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java @@ -93,8 +93,8 @@ public XAddArgs limit(long limit) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (nomkstream) { args.add("NOMKSTREAM"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java index 3538cc9c816878..af3a1da6ec7ec1 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java @@ -97,8 +97,8 @@ public XClaimArgs lastId(String lastId) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (idle != null) { args.add("IDLE"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java index db51b9b4743641..9656562661840b 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java @@ -40,8 +40,8 @@ public XGroupCreateArgs entriesRead(String id) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (mkstream) { args.add("MKSTREAM"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java index f5a00766e58f4b..4913bfbc3ced5b 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java @@ -29,8 +29,8 @@ public XGroupSetIdArgs entriesRead(long id) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (entriesRead > 0) { args.add("ENTRIESREAD"); args.add(Long.toString(entriesRead)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingArgs.java index e5c1db2afadbfd..5b5b30a41112fe 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingArgs.java @@ -39,8 +39,8 @@ public Duration idle() { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (owner != null) { args.add(owner); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java index 630b89b22f4eaa..151a9e6629a9ea 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java @@ -38,8 +38,8 @@ public XReadArgs block(Duration block) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (count > 0) { args.add("COUNT"); args.add(Integer.toString(count)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java index 7fc78998e4d502..26be832fcaeb25 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java @@ -51,8 +51,8 @@ public XReadGroupArgs noack() { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (count > 0) { args.add("COUNT"); args.add(Integer.toString(count)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java index 81ef8a74f097dc..3d33cc56a2e8ed 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java @@ -63,8 +63,8 @@ public XTrimArgs limit(long limit) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (maxlen > 0) { if (minid != null) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/GetExArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/GetExArgs.java index 85a743b0e01c34..2e82f9dc3afec4 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/GetExArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/GetExArgs.java @@ -128,8 +128,8 @@ public GetExArgs persist() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (ex >= 0) { args.add("EX"); args.add(Long.toString(ex)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/SetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/SetArgs.java index 7d23e48f075a69..8d9100e15eaab3 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/SetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/SetArgs.java @@ -168,8 +168,8 @@ public SetArgs get() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (ex >= 0) { args.add("EX"); args.add(Long.toString(ex)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AddArgs.java index 3063daff7cfcef..15386a16a66570 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AddArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AddArgs.java @@ -97,8 +97,8 @@ public AddArgs label(String label, Object value) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (retention != null) { list.add("RETENTION"); if (retention == Duration.ZERO) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AlterArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AlterArgs.java index d3268d4b8ed3a1..25664e594b395f 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AlterArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AlterArgs.java @@ -92,8 +92,8 @@ public AlterArgs label(String label, Object value) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (retention != null) { list.add("RETENTION"); if (retention == Duration.ZERO) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/CreateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/CreateArgs.java index d908c15de00590..48903a55d32c96 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/CreateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/CreateArgs.java @@ -123,8 +123,8 @@ public CreateArgs label(String label, Object value) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (retention != null) { list.add("RETENTION"); if (retention == Duration.ZERO) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/IncrementArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/IncrementArgs.java index 33204a20c344d8..51442aaef1a900 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/IncrementArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/IncrementArgs.java @@ -95,8 +95,8 @@ public IncrementArgs label(String label, Object value) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (timestamp >= 0) { list.add("TIMESTAMP"); list.add(Long.toString(timestamp)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MGetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MGetArgs.java index 77a26768b54173..142e3ea077fe0e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MGetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MGetArgs.java @@ -56,8 +56,8 @@ public MGetArgs selectedLabel(String label) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (latest) { list.add("LATEST"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MRangeArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MRangeArgs.java index 6b70a1ca52f5d0..f0735aff1b8ee9 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MRangeArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MRangeArgs.java @@ -203,8 +203,8 @@ public MRangeArgs groupBy(String label, Reducer reducer) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (latest) { list.add("LATEST"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/RangeArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/RangeArgs.java index 9cded0a3d0d4e0..71627f97a179c1 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/RangeArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/RangeArgs.java @@ -154,8 +154,8 @@ public RangeArgs empty() { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (latest) { list.add("LATEST"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/GetExArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/GetExArgs.java index 5966a68c43d41a..5bcdafdd942d84 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/GetExArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/GetExArgs.java @@ -125,8 +125,8 @@ public GetExArgs persist() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (ex >= 0) { args.add("EX"); args.add(Long.toString(ex)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/SetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/SetArgs.java index c03af517867a49..e659cd0e25121c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/SetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/SetArgs.java @@ -165,8 +165,8 @@ public SetArgs get() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (ex >= 0) { args.add("EX"); args.add(Long.toString(ex)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java index d58075205f90b4..402784fe8e9491 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java @@ -19,6 +19,56 @@ public static X[] notNullOrEmpty(X[] array, String name) { return array; } + public static float[] notNullOrEmpty(float[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + + public static double[] notNullOrEmpty(double[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + + public static int[] notNullOrEmpty(int[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + + public static long[] notNullOrEmpty(long[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + + public static byte[] notNullOrEmpty(byte[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + public static String notNullOrBlank(String v, String name) { if (v == null) { throw new IllegalArgumentException("`" + name + "` must not be `null`"); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java index f4f124f9164723..8388499e94a7f7 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java @@ -4,8 +4,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Duration; +import java.util.HashMap; import java.util.Map; import org.assertj.core.data.Offset; @@ -16,6 +18,7 @@ import io.quarkus.redis.datasource.hash.HashCommands; import io.quarkus.redis.datasource.search.AggregateArgs; import io.quarkus.redis.datasource.search.CreateArgs; +import io.quarkus.redis.datasource.search.Document; import io.quarkus.redis.datasource.search.FieldOptions; import io.quarkus.redis.datasource.search.FieldType; import io.quarkus.redis.datasource.search.HighlightArgs; @@ -23,10 +26,13 @@ import io.quarkus.redis.datasource.search.NumericFilter; import io.quarkus.redis.datasource.search.QueryArgs; import io.quarkus.redis.datasource.search.SearchCommands; +import io.quarkus.redis.datasource.search.SearchQueryResponse; import io.quarkus.redis.datasource.search.SpellCheckArgs; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.redis.client.Command; +import io.vertx.mutiny.redis.client.Request; /** * Lots of tests are using await().untilAsserted as the indexing process runs in the background. @@ -843,4 +849,68 @@ void testSynonymsInQueries() { assertThat(res.documents()).anySatisfy(d -> assertThat(d.property("t").asString()).isEqualTo("hello")); assertThat(res.documents()).anySatisfy(d -> assertThat(d.property("t").asString()).isEqualTo("world")); } + + @Test + void testKNearestNeighborsDouble() { + redis.sendAndAwait(Request.cmd(Command.FT_CREATE) + .arg("IDX:double") + .arg("ON").arg("JSON") + .arg("PREFIX").arg("1").arg("indexed:") + .arg("SCHEMA") + .arg("$.vector").arg("AS").arg("vector").arg("VECTOR").arg("HNSW") + .arg("6").arg("DIM").arg("6").arg("DISTANCE_METRIC").arg("COSINE").arg("TYPE").arg("FLOAT64")); + + double[] queryVector = new double[] { 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 }; + + ds.json().jsonSet("indexed:1", "$", createDocument(new double[] { 1.0, 0.0, 0.0, 0.0, 0.0, 0.0 })); + ds.json().jsonSet("indexed:2", "$", createDocument(new double[] { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0 })); + ds.json().jsonSet("indexed:3", "$", createDocument(new double[] { 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 })); + ds.json().jsonSet("indexed:4", "$", createDocument(new double[] { 0.0, 0.0, 0.0, 1.0, 0.0, 0.0 })); + + String query = "*=>[ KNN 1 @vector $BLOB AS vector_score ]"; + + QueryArgs args = new QueryArgs() + .sortByAscending("vector_score") + .param("DIALECT", "2") + .param("BLOB", queryVector); + SearchQueryResponse response = ds.search().ftSearch("IDX:double", query, args); + assertEquals(1, response.count()); + Document foundEntry = response.documents().get(0); + assertEquals("indexed:3", foundEntry.key()); + } + + @Test + void testKNearestNeighborsFloat() { + redis.sendAndAwait(Request.cmd(Command.FT_CREATE) + .arg("IDX:float") + .arg("ON").arg("JSON") + .arg("PREFIX").arg("1").arg("indexed:") + .arg("SCHEMA") + .arg("$.vector").arg("AS").arg("vector").arg("VECTOR").arg("HNSW") + .arg("6").arg("DIM").arg("6").arg("DISTANCE_METRIC").arg("COSINE").arg("TYPE").arg("FLOAT32")); + + float[] queryVector = new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f }; + + ds.json().jsonSet("indexed:1", "$", createDocument(new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f })); + ds.json().jsonSet("indexed:2", "$", createDocument(new float[] { 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f })); + ds.json().jsonSet("indexed:3", "$", createDocument(new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f })); + ds.json().jsonSet("indexed:4", "$", createDocument(new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f })); + + String query = "*=>[ KNN 1 @vector $BLOB AS vector_score ]"; + QueryArgs args = new QueryArgs() + .sortByAscending("vector_score") + .param("DIALECT", "2") + .param("BLOB", queryVector); + + SearchQueryResponse response = ds.search().ftSearch("IDX:float", query, args); + assertEquals(1, response.count()); + Document foundEntry = response.documents().get(0); + assertEquals("indexed:3", foundEntry.key()); + } + + private Map createDocument(Object embedding) { + Map fields = new HashMap<>(); + fields.put("vector", embedding); + return fields; + } } From 53c799c720a139430108f858e5dadd62a6366572 Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Wed, 8 Nov 2023 09:42:21 +0100 Subject: [PATCH 12/68] Add arguments for creating Redis vector fields --- .../datasource/search/DistanceMetric.java | 10 ++ .../redis/datasource/search/FieldOptions.java | 116 ++++++++++++++++++ .../redis/datasource/search/QueryArgs.java | 5 + .../datasource/search/VectorAlgorithm.java | 18 +++ .../redis/datasource/search/VectorType.java | 15 +++ .../redis/datasource/SearchCommandsTest.java | 39 +++--- 6 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/DistanceMetric.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorAlgorithm.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorType.java diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/DistanceMetric.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/DistanceMetric.java new file mode 100644 index 00000000000000..97ffea48e689b4 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/DistanceMetric.java @@ -0,0 +1,10 @@ +package io.quarkus.redis.datasource.search; + +/** + * Metric for computing the distance between two vectors. + */ +public enum DistanceMetric { + L2, + IP, + COSINE +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/FieldOptions.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/FieldOptions.java index f640ee22ad3d3e..f19720735286cd 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/FieldOptions.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/FieldOptions.java @@ -17,6 +17,12 @@ public class FieldOptions { private char separator; private boolean caseSensitive; private boolean withSuffixTrie; + private VectorAlgorithm vectorAlgorithm; + private VectorType vectorType; + private Integer dimension; + private DistanceMetric distanceMetric; + private Integer initialCap; + private Integer blockSize; /** * Numeric, tag (not supported with JSON) or text attributes can have the optional SORTABLE argument. @@ -125,8 +131,98 @@ public FieldOptions withSuffixTrie() { return this; } + /** + * For vector fields, specifies the vector algorithm to use when searching k most similar vectors in an index. + * + * @param vectorAlgorithm the vector algorithm + * @return the current {@code FieldOptions} + */ + public FieldOptions vectorAlgorithm(VectorAlgorithm vectorAlgorithm) { + this.vectorAlgorithm = vectorAlgorithm; + return this; + } + + /** + * For vector fields, specifies the vector type. + * + * @param vectorType the vector type + * @return the current {@code FieldOptions} + */ + public FieldOptions vectorType(VectorType vectorType) { + this.vectorType = vectorType; + return this; + } + + /** + * For vector fields, specifies the dimension. + * + * @param dimension the dimension + * @return the current {@code FieldOptions} + */ + public FieldOptions dimension(int dimension) { + this.dimension = dimension; + return this; + } + + /** + * For vector fields, specifies the distance metric. + * + * @param distanceMetric the distance metric + * @return the current {@code FieldOptions} + */ + public FieldOptions distanceMetric(DistanceMetric distanceMetric) { + this.distanceMetric = distanceMetric; + return this; + } + + /** + * For vector fields, specifies the initial vector capacity in the index. + * + * @param initialCap the initial capacity + * @return the current {@code FieldOptions} + */ + public FieldOptions initialCap(int initialCap) { + this.initialCap = initialCap; + return this; + } + + /** + * For vector fields, specifies the block size (the amount of vectors to store in a contiguous array). + * + * @param blockSize the block size + * @return the current {@code FieldOptions} + */ + public FieldOptions blockSize(int blockSize) { + this.blockSize = blockSize; + return this; + } + public List toArgs() { List list = new ArrayList<>(); + if (vectorAlgorithm != null) { + list.add(vectorAlgorithm.name()); + list.add(String.valueOf(vectorSimilarityArgumentsCount())); + } + if (vectorType != null) { + list.add("TYPE"); + list.add(vectorType.name()); + } + if (dimension != null) { + list.add("DIM"); + list.add(dimension.toString()); + } + if (distanceMetric != null) { + list.add("DISTANCE_METRIC"); + list.add(distanceMetric.name()); + } + if (initialCap != null) { + list.add("INITIAL_CAP"); + list.add(initialCap.toString()); + } + if (blockSize != null) { + list.add("BLOCK_SIZE"); + list.add(blockSize.toString()); + } if (sortable) { list.add("SORTABLE"); } @@ -162,4 +258,24 @@ public List toArgs() { } return list; } + + private int vectorSimilarityArgumentsCount() { + int count = 0; + if (vectorType != null) { + count += 2; + } + if (dimension != null) { + count += 2; + } + if (distanceMetric != null) { + count += 2; + } + if (initialCap != null) { + count += 2; + } + if (blockSize != null) { + count += 2; + } + return count; + } } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java index 48b425bd3b4e8c..18f1b65106cb92 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java @@ -364,6 +364,7 @@ public QueryArgs param(String name, String value) { /** * Defines a parameter with a byte array value. + * * @param name the parameter name * @param value the parameter value as byte array * @return the current {@code QueryArgs} @@ -375,6 +376,7 @@ public QueryArgs param(String name, byte[] value) { /** * Defines a parameter with a float array value. + * * @param name the parameter name * @param value the parameter value as array of floats * @return the current {@code QueryArgs} @@ -386,6 +388,7 @@ public QueryArgs param(String name, float[] value) { /** * Defines a parameter with a double array value. + * * @param name the parameter name * @param value the parameter value as array of doubles * @return the current {@code QueryArgs} @@ -397,6 +400,7 @@ public QueryArgs param(String name, double[] value) { /** * Defines a parameter with an int array value. + * * @param name the parameter name * @param value the parameter value as array of ints * @return the current {@code QueryArgs} @@ -408,6 +412,7 @@ public QueryArgs param(String name, int[] value) { /** * Defines a parameter with a long array value. + * * @param name the parameter name * @param value the parameter value as array of longs * @return the current {@code QueryArgs} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorAlgorithm.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorAlgorithm.java new file mode 100644 index 00000000000000..6c77a872aa5492 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorAlgorithm.java @@ -0,0 +1,18 @@ +package io.quarkus.redis.datasource.search; + +/** + * The vector algorithm to use when searching k most similar vectors in an index. + */ +public enum VectorAlgorithm { + + /** + * Brute force algorithm. + */ + FLAT, + + /** + * Hierarchical Navigable Small World Graph algorithm. + */ + HNSW + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorType.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorType.java new file mode 100644 index 00000000000000..c5fb2d049c3597 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorType.java @@ -0,0 +1,15 @@ +package io.quarkus.redis.datasource.search; + +/** + * Type of vector stored in a vector field. + */ +public enum VectorType { + /** + * A 32-bit floating point number. + */ + FLOAT32, + /** + * A 64-bit floating point number. + */ + FLOAT64 +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java index 8388499e94a7f7..cb3b5bbefa17e2 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java @@ -18,6 +18,7 @@ import io.quarkus.redis.datasource.hash.HashCommands; import io.quarkus.redis.datasource.search.AggregateArgs; import io.quarkus.redis.datasource.search.CreateArgs; +import io.quarkus.redis.datasource.search.DistanceMetric; import io.quarkus.redis.datasource.search.Document; import io.quarkus.redis.datasource.search.FieldOptions; import io.quarkus.redis.datasource.search.FieldType; @@ -28,11 +29,11 @@ import io.quarkus.redis.datasource.search.SearchCommands; import io.quarkus.redis.datasource.search.SearchQueryResponse; import io.quarkus.redis.datasource.search.SpellCheckArgs; +import io.quarkus.redis.datasource.search.VectorAlgorithm; +import io.quarkus.redis.datasource.search.VectorType; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import io.vertx.mutiny.redis.client.Command; -import io.vertx.mutiny.redis.client.Request; /** * Lots of tests are using await().untilAsserted as the indexing process runs in the background. @@ -852,13 +853,16 @@ void testSynonymsInQueries() { @Test void testKNearestNeighborsDouble() { - redis.sendAndAwait(Request.cmd(Command.FT_CREATE) - .arg("IDX:double") - .arg("ON").arg("JSON") - .arg("PREFIX").arg("1").arg("indexed:") - .arg("SCHEMA") - .arg("$.vector").arg("AS").arg("vector").arg("VECTOR").arg("HNSW") - .arg("6").arg("DIM").arg("6").arg("DISTANCE_METRIC").arg("COSINE").arg("TYPE").arg("FLOAT64")); + ds.search().ftCreate("IDX:double", + new CreateArgs() + .onJson() + .prefixes("indexed:") + .indexedField("$.vector", "vector", FieldType.VECTOR, + new FieldOptions() + .vectorAlgorithm(VectorAlgorithm.HNSW) + .dimension(6) + .distanceMetric(DistanceMetric.COSINE) + .vectorType(VectorType.FLOAT64))); double[] queryVector = new double[] { 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 }; @@ -881,13 +885,16 @@ void testKNearestNeighborsDouble() { @Test void testKNearestNeighborsFloat() { - redis.sendAndAwait(Request.cmd(Command.FT_CREATE) - .arg("IDX:float") - .arg("ON").arg("JSON") - .arg("PREFIX").arg("1").arg("indexed:") - .arg("SCHEMA") - .arg("$.vector").arg("AS").arg("vector").arg("VECTOR").arg("HNSW") - .arg("6").arg("DIM").arg("6").arg("DISTANCE_METRIC").arg("COSINE").arg("TYPE").arg("FLOAT32")); + ds.search().ftCreate("IDX:float", + new CreateArgs() + .onJson() + .prefixes("indexed:") + .indexedField("$.vector", "vector", FieldType.VECTOR, + new FieldOptions() + .vectorAlgorithm(VectorAlgorithm.HNSW) + .dimension(6) + .distanceMetric(DistanceMetric.COSINE) + .vectorType(VectorType.FLOAT32))); float[] queryVector = new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f }; From 2d37aa4d41a1198d8b8eebd1b59eb825b171f6d6 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 13 Oct 2023 16:59:04 +0200 Subject: [PATCH 13/68] Enable publication of build scans for PRs coming from forks --- .github/workflows/ci-actions-incremental.yml | 58 ++++++++++++++++++- .../develocity-publish-build-scans.yml | 24 ++++++++ .github/workflows/develocity-verify-tos.yml | 33 +++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/develocity-publish-build-scans.yml create mode 100644 .github/workflows/develocity-verify-tos.yml diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 85ae65268d611a..47fa67d932ce2b 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -199,6 +199,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Initial JDK 11 Build" calculate-test-jobs: name: Calculate Test Jobs @@ -388,6 +393,11 @@ jobs: path: | **/build.log retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "JVM Tests - JDK ${{matrix.java.name}}" maven-tests: name: Maven Tests - JDK ${{matrix.java.name}} @@ -445,7 +455,6 @@ jobs: distribution: temurin java-version: ${{ matrix.java.java-version }} - name: Build - # Important: keep -pl ... in sync with "Calculate run flags"! # Despite the pre-calculated run_jvm flag, GIB has to be re-run here to figure out the exact submodules to build. run: ./mvnw $COMMON_MAVEN_ARGS $JVM_TEST_MAVEN_ARGS clean install -pl 'integration-tests/maven' -pl 'integration-tests/devmode' ${{ needs.build-jdk11.outputs.gib_args }} @@ -477,6 +486,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Maven Tests - JDK ${{matrix.java.name}}" gradle-tests: name: Gradle Tests - JDK ${{matrix.java.name}} @@ -549,6 +563,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Gradle Tests - JDK ${{matrix.java.name}}" devtools-tests: name: Devtools Tests - JDK ${{matrix.java.name}} @@ -627,6 +646,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Devtools Tests - JDK ${{matrix.java.name}}" kubernetes-tests: name: Kubernetes Tests - JDK ${{matrix.java.name}} @@ -655,6 +679,10 @@ jobs: os-name: "windows-latest" } steps: + - name: Gradle Enterprise environment + run: | + echo "GE_TAGS=jdk-${{matrix.java.name}}" >> "$GITHUB_ENV" + echo "GE_CUSTOM_VALUES=gh-job-name=Kubernetes Tests - JDK ${{matrix.java.name}}" >> "$GITHUB_ENV" - name: Support longpaths on Windows if: "startsWith(matrix.java.os-name, 'windows')" run: git config --global core.longpaths true @@ -700,6 +728,11 @@ jobs: target/build-report.json LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Kubernetes Tests - JDK ${{matrix.java.name}}" quickstarts-tests: name: Quickstarts Compilation - JDK ${{matrix.java.name}} @@ -756,6 +789,12 @@ jobs: quarkus-quickstarts/target/build-report.json quarkus-quickstarts/LICENSE retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Quickstarts Compilation - JDK ${{matrix.java.name}}" + virtual-thread-native-tests: name: Native Tests - Virtual Thread - ${{matrix.category}} runs-on: ${{matrix.os-name}} @@ -812,6 +851,12 @@ jobs: integration-tests/virtual-threads/target/build-report.json integration-tests/virtual-threads/target/gradle-build-scan-url.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Native Tests - Virtual Thread - ${{matrix.category}}" + tcks-test: name: MicroProfile TCKs Tests needs: [build-jdk11, calculate-test-jobs] @@ -819,7 +864,6 @@ jobs: if: "needs.calculate-test-jobs.outputs.run_tcks == 'true' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))" runs-on: ubuntu-latest timeout-minutes: 150 - steps: - name: Gradle Enterprise environment run: | @@ -879,6 +923,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "MicroProfile TCKs Tests" native-tests: name: Native Tests - ${{matrix.category}} @@ -971,6 +1020,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Native Tests - ${{matrix.category}}" build-report: runs-on: ubuntu-latest diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml new file mode 100644 index 00000000000000..4c3b1e03d231f2 --- /dev/null +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -0,0 +1,24 @@ +name: Develocity - Publish Maven Build Scans + +on: + workflow_run: + workflows: [ "Quarkus CI" ] + types: [ completed ] + +jobs: + publish-build-scans: + if: github.repository == 'quarkusio/quarkus' + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Verify Terms of Service acceptance job passed + uses: gradle/github-actions/terms-of-service-acceptance/verify@v1-beta + with: + terms-of-service-acceptance-workflow-job-name: 'run-terms-of-service-acceptance' + - name: Publish Maven Build Scans + uses: gradle/github-actions/maven-build-scan/publish@v1-beta + with: + develocity-url: 'https://ge.quarkus.io' + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + skip-comment: true \ No newline at end of file diff --git a/.github/workflows/develocity-verify-tos.yml b/.github/workflows/develocity-verify-tos.yml new file mode 100644 index 00000000000000..7533088f1db218 --- /dev/null +++ b/.github/workflows/develocity-verify-tos.yml @@ -0,0 +1,33 @@ +name: Develocity - Terms of Service acceptance verification + +on: + # issue_comment event is triggered when a pull-request is commented + issue_comment: + types: [ created ] + pull_request_target: + +jobs: + run-terms-of-service-acceptance: + if: github.repository == 'quarkusio/quarkus' + runs-on: ubuntu-latest + permissions: + # required to update signature file + contents: write + # required to comment pull-request + pull-requests: write + # required to update pull-request status check + actions: write + statuses: write + steps: + - name: Run Terms of Service acceptance verification + uses: gradle/github-actions/terms-of-service-acceptance/run@v1-beta + with: + # tos-location can also point to a file in a Github repository with this syntax: ///blob//tos.html + tos-location: 'https://foo.bar/tos.html' + # Optional inputs + pr-comment-tos-acceptance-missing: 'Quarkus uses Develocity to provide insights about CI builds. Please accept [Develocity Terms Of Service]({0}) to get your pull request Build Scan published by commenting this pull-request with the following message:' + #pr-comment-tos-acceptance-request: 'I have read Develocity Terms Of Service and I hereby accept the Terms' + pr-comment-tos-acceptance-validation: 'All contributors have accepted Develocity Terms Of Service.' + # let's whitelist the most active contributors from Red Hat + white-list: 'gsmet,geoand,stuartwdouglas,gastaldi,mkouba,cescoffier,sberyozkin,Sanne,aloubyansky,yrodiere,FroMage,phillip-kruger,machi1990,dmlloyd,iocanel,Sgitario,ia3andy,radcortez,rsvoboda,Ladicek,zakkak,ebullient,jmartisk,gwenneg,manovotn,maxandersen,starksm64,patriot1burke,emmanuelbernard,ozangunalp,michalvavrik,n1hility,tsegismont,galderz,ppalaga,evanchooly,holly-cummins,michelle-purcell,jponge,DavideD,karesti,brunobat,stalep,Karm,manusa,pedroigor,aureamunoz,metacosm,johnaohara,MichalMaler,MikeEdgar,alesj' + white-list-only: true \ No newline at end of file From 6b16b10ecc3beb15ebcb33879a5b03f22187d7be Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Wed, 8 Nov 2023 12:32:52 +0200 Subject: [PATCH 14/68] Option TraceServiceLoaderFeature removed in GraalVM 23.1 The option `TraceServiceLoaderFeature` is no longer available starting with GraalVM 23.1 as part of a refactoring in how the `ServiceLoaderFeature` works. There is currently no option offering similar output. Fixes https://github.com/quarkusio/quarkus/issues/36129 --- .../quarkus/deployment/pkg/steps/NativeImageBuildStep.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index c4a367b89fceeb..77ad5a2dc78ebd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -911,8 +911,10 @@ public NativeImageInvokerInfo build() { if (nativeConfig.autoServiceLoaderRegistration()) { addExperimentalVMOption(nativeImageArgs, "-H:+UseServiceLoaderFeature"); - //When enabling, at least print what exactly is being added: - nativeImageArgs.add("-H:+TraceServiceLoaderFeature"); + if (graalVMVersion.compareTo(GraalVM.Version.VERSION_23_1_0) < 0) { + // When enabling, at least print what exactly is being added. Only possible in <23.1.0 + nativeImageArgs.add("-H:+TraceServiceLoaderFeature"); + } } else { addExperimentalVMOption(nativeImageArgs, "-H:-UseServiceLoaderFeature"); } From 4000072d054317495d514f165d94c82b32df9983 Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Wed, 8 Nov 2023 14:42:19 +0200 Subject: [PATCH 15/68] Bump wildfly-common version to 1.7.0.Final Closes https://github.com/quarkusio/quarkus/issues/36686 --- 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 a05b6e0257fc22..12ba92c88e127e 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -116,7 +116,7 @@ 2.2.1.Final 2.0.6 2.0.0.Final - 1.5.4.Final-format-001 + 1.7.0.Final 1.0.1.Final 2.2.2.Final 3.5.1.Final From d501eee211d4f681297ff2277aab494bb85cc3ed Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Wed, 8 Nov 2023 13:47:19 +0100 Subject: [PATCH 16/68] Simplify build scan workflow by publishing only for approved developers --- .../develocity-preapproved-developers.json | 56 +++++++++++++++++++ .../develocity-publish-build-scans.yml | 28 ++++++++-- .github/workflows/develocity-verify-tos.yml | 33 ----------- 3 files changed, 78 insertions(+), 39 deletions(-) create mode 100644 .github/develocity-preapproved-developers.json delete mode 100644 .github/workflows/develocity-verify-tos.yml diff --git a/.github/develocity-preapproved-developers.json b/.github/develocity-preapproved-developers.json new file mode 100644 index 00000000000000..670c4d84f00ac1 --- /dev/null +++ b/.github/develocity-preapproved-developers.json @@ -0,0 +1,56 @@ +{ + "preapproved-developers": [ + "alesj", + "aloubyansky", + "aureamunoz", + "brunobat", + "cescoffier", + "DavideD", + "dmlloyd", + "ebullient", + "emmanuelbernard", + "evanchooly", + "FroMage", + "galderz", + "gastaldi", + "geoand", + "gsmet", + "gwenneg", + "holly-cummins", + "ia3andy", + "iocanel", + "jmartisk", + "johnaohara", + "jponge", + "karesti", + "Karm", + "Ladicek", + "machi1990", + "manovotn", + "manusa", + "maxandersen", + "metacosm", + "MichalMaler", + "michalvavrik", + "michelle-purcell", + "MikeEdgar", + "mkouba", + "n1hility", + "ozangunalp", + "patriot1burke", + "pedroigor", + "phillip-kruger", + "ppalaga", + "radcortez", + "rsvoboda", + "Sanne", + "sberyozkin", + "Sgitario", + "stalep", + "starksm64", + "stuartwdouglas", + "tsegismont", + "yrodiere", + "zakkak" + ] +} \ No newline at end of file diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml index 4c3b1e03d231f2..e56ea04a0d47a2 100644 --- a/.github/workflows/develocity-publish-build-scans.yml +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -5,20 +5,36 @@ on: workflows: [ "Quarkus CI" ] types: [ completed ] +defaults: + run: + shell: bash + jobs: publish-build-scans: - if: github.repository == 'quarkusio/quarkus' + if: github.repository == 'quarkusio/quarkus' && github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest permissions: pull-requests: write steps: - - name: Verify Terms of Service acceptance job passed - uses: gradle/github-actions/terms-of-service-acceptance/verify@v1-beta - with: - terms-of-service-acceptance-workflow-job-name: 'run-terms-of-service-acceptance' + - uses: actions/checkout@v4 + - name: Extract preapproved developers list + id: extract-preapproved-developers + run: | + preapproveddevelopers=$(cat .github/develocity-preapproved-developers.json) + echo "preapproved-developpers=${preapproveddevelopers}" >> $GITHUB_OUTPUT - name: Publish Maven Build Scans uses: gradle/github-actions/maven-build-scan/publish@v1-beta + if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} with: develocity-url: 'https://ge.quarkus.io' develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - skip-comment: true \ No newline at end of file + skip-comment: true + - name: Push to summary + run: | + echo -n "Pull request: " >> ${GITHUB_STEP_SUMMARY} + cat pr-number.out >> ${GITHUB_STEP_SUMMARY} + echo >> ${GITHUB_STEP_SUMMARY} + echo >> ${GITHUB_STEP_SUMMARY} + echo "| Job | Status | Build scan |" >> ${GITHUB_STEP_SUMMARY} + echo "|---|---|---|" >> ${GITHUB_STEP_SUMMARY} + cat publication.out >> ${GITHUB_STEP_SUMMARY} diff --git a/.github/workflows/develocity-verify-tos.yml b/.github/workflows/develocity-verify-tos.yml deleted file mode 100644 index 7533088f1db218..00000000000000 --- a/.github/workflows/develocity-verify-tos.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Develocity - Terms of Service acceptance verification - -on: - # issue_comment event is triggered when a pull-request is commented - issue_comment: - types: [ created ] - pull_request_target: - -jobs: - run-terms-of-service-acceptance: - if: github.repository == 'quarkusio/quarkus' - runs-on: ubuntu-latest - permissions: - # required to update signature file - contents: write - # required to comment pull-request - pull-requests: write - # required to update pull-request status check - actions: write - statuses: write - steps: - - name: Run Terms of Service acceptance verification - uses: gradle/github-actions/terms-of-service-acceptance/run@v1-beta - with: - # tos-location can also point to a file in a Github repository with this syntax: ///blob//tos.html - tos-location: 'https://foo.bar/tos.html' - # Optional inputs - pr-comment-tos-acceptance-missing: 'Quarkus uses Develocity to provide insights about CI builds. Please accept [Develocity Terms Of Service]({0}) to get your pull request Build Scan published by commenting this pull-request with the following message:' - #pr-comment-tos-acceptance-request: 'I have read Develocity Terms Of Service and I hereby accept the Terms' - pr-comment-tos-acceptance-validation: 'All contributors have accepted Develocity Terms Of Service.' - # let's whitelist the most active contributors from Red Hat - white-list: 'gsmet,geoand,stuartwdouglas,gastaldi,mkouba,cescoffier,sberyozkin,Sanne,aloubyansky,yrodiere,FroMage,phillip-kruger,machi1990,dmlloyd,iocanel,Sgitario,ia3andy,radcortez,rsvoboda,Ladicek,zakkak,ebullient,jmartisk,gwenneg,manovotn,maxandersen,starksm64,patriot1burke,emmanuelbernard,ozangunalp,michalvavrik,n1hility,tsegismont,galderz,ppalaga,evanchooly,holly-cummins,michelle-purcell,jponge,DavideD,karesti,brunobat,stalep,Karm,manusa,pedroigor,aureamunoz,metacosm,johnaohara,MichalMaler,MikeEdgar,alesj' - white-list-only: true \ No newline at end of file From 9bae9a718d0f03e24f27aec606cf8b4602a1dd0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Sapp=C3=A9=20Griot?= Date: Wed, 8 Nov 2023 16:31:37 +0100 Subject: [PATCH 17/68] Issue 36882 --- extensions/narayana-jta/runtime/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/narayana-jta/runtime/pom.xml b/extensions/narayana-jta/runtime/pom.xml index 7fb2ff30c5230a..8b3389ee5881fc 100644 --- a/extensions/narayana-jta/runtime/pom.xml +++ b/extensions/narayana-jta/runtime/pom.xml @@ -73,6 +73,10 @@ jakarta.enterprise jakarta.enterprise.lang-model + + jakarta.ejb + jakarta.ejb-api + From a6f7424c1b3726f05c49b8390f544eed9fc4b850 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Wed, 8 Nov 2023 18:11:11 +0100 Subject: [PATCH 18/68] Better handle multi-line outputs in develocity-publish-build-scans.yml --- .github/workflows/develocity-publish-build-scans.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml index e56ea04a0d47a2..b269a77a6c5603 100644 --- a/.github/workflows/develocity-publish-build-scans.yml +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -20,8 +20,9 @@ jobs: - name: Extract preapproved developers list id: extract-preapproved-developers run: | - preapproveddevelopers=$(cat .github/develocity-preapproved-developers.json) - echo "preapproved-developpers=${preapproveddevelopers}" >> $GITHUB_OUTPUT + echo "preapproved-developpers<> $GITHUB_OUTPUT + cat .github/develocity-preapproved-developers.json >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT - name: Publish Maven Build Scans uses: gradle/github-actions/maven-build-scan/publish@v1-beta if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} From 85b8e53369fad33e7fe48b19fd764544bdbe0462 Mon Sep 17 00:00:00 2001 From: brunobat Date: Mon, 30 Oct 2023 18:21:01 +0000 Subject: [PATCH 19/68] OpenTracing to OpenTelemetry Guide --- .../src/main/asciidoc/images/ot-to-otel-1.png | Bin 0 -> 161279 bytes .../src/main/asciidoc/images/ot-to-otel-2.png | Bin 0 -> 289242 bytes .../src/main/asciidoc/images/ot-to-otel-3.png | Bin 0 -> 249835 bytes docs/src/main/asciidoc/opentelemetry.adoc | 33 +- docs/src/main/asciidoc/opentracing.adoc | 2 +- ...elemetry-opentracing-to-otel-tutorial.adoc | 544 ++++++++++++++++++ 6 files changed, 575 insertions(+), 4 deletions(-) create mode 100644 docs/src/main/asciidoc/images/ot-to-otel-1.png create mode 100644 docs/src/main/asciidoc/images/ot-to-otel-2.png create mode 100644 docs/src/main/asciidoc/images/ot-to-otel-3.png create mode 100644 docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc diff --git a/docs/src/main/asciidoc/images/ot-to-otel-1.png b/docs/src/main/asciidoc/images/ot-to-otel-1.png new file mode 100644 index 0000000000000000000000000000000000000000..56bc6307500575f0619a67c79b175a838af57ee7 GIT binary patch literal 161279 zcmb@u2UJr_7d8xt1;L69ML?xUM|un8Vj*(j;Jkjyz}X3w5Id+&Lk{aj7u-ihOvkCTy+oq)>U zRwpAn7E4A(i8*!z_=M}~(|$5CauX{#IW?%99Fv-pgN2nXjEwB&YrBvZPQSXzO0_59H2pkRa$tsB#upK&Q zFEDX)^lA(WCY2|qJ=xLT=;Efire36_YV~;3{eek3hpP6H0#bRWw?KJXj-7XthMckll`6zWfQ6!_~b@bhOP#n0Bq zViOPjyr#qe-;v2`$U&jNZw)gi7|h=Jk%NnQ0Z#((0hOb?o--NQ1s3p!9IDRzgN%%# z*Gf~@MORr#+|0p_=YhGyLl}>nog?@hGD$aa;L;A}@_@-Xu<7_uy=Xj#%=F>WA7n9pK}}LZ02O;=wjty&jddA z!9xeQ3*`EB@I`BT%`CVdH?O>|GrD_KetKU z5(LJ#d(%HZ{qH+;|MiZ4e)`{cs5)5zf_MOSP3qSE8~*v(etSt?aPt45411ck`zfGm zQpY8E|5jV6nA*-!Z2WJb3K8Wdee z7xTN^VD1nye-RQ$1*<*%SVN@e>m9n#(F+`M{5F_{q!7i$jUlg>mIjsy`NUrL1ka&n z_f;avqq)}|SJvHU<4NqZfy^SdTku?B8*{ljnrF!=D36_HlJOz?#f2|9Ww)-`D{E^X zQsW=LY`OoC#0$FR9*F$DKAQ}q3SEaSLe|e$*t_@+8SH_j?S8$(>?0+tDtTslJ zX0Ic@&zPAUaze^a^zXfk)xgx+THzEM%JbM)_uu!n(vdAuKYdg&)z8=d?;)bN znMTZWD!koQe?PEG`lh7i38W~Ue$(&!LFP+!p|z@x(wp=o?pMOy6B%DxW#@Zp{h;5s zlriN$Oc|>*NwmiuV0bsE(si@no^|9u8~5yg3*m1SK(PbT;ID9XS3ZC=^i1nQSpaG1 zwLJa%0sr&Dm+IVUM}A+E5+l}MD;p?}#ygcE;?8ZkT)$At{jW?Z4zK)QJXhuv+ku~( zoSdANN;Tl{LFxCxVB&jQnFDK}j17EbdF%JeQF;3~mr;pkW!-Hr{FCV4Pdf1Q<%x#F z2YX>C%3vHsRlus1yw6DgDvSDK@+ZSfV|Qa+c{kYa|L>Q`1JkDgFg_0T=w|@k@(Fud)*mmE_6pZlbC@T1f1lPRj zV=6;=_9ymWsq{9;2b2rM)b82fQkQy!1G*rZrO1{5+Dekb- zA-$!M-z40x89?tOpWk-(3Ow2Ur|s8`q8KJMyQayVg5+)2cBn{X=zDJw0@l~OHwwM6 z<;1c@Enss|C2USfd%uS@G%g}q+|y2Z{qQ3+j7QnKe&`P?wD45go(aQwOr+z5EQ_}0 z9Zbr$2{;FpwD^sIDQz9Y$6xZS?0AirUg&5V*cZe44U+c-b>kSu^!p7)l03Xy&r(Zz zbM%yijeKz(&xbkntB#fAT^SCZv&}0DY|^AU)ij_do^Y>m?;WDIgv0Ozk83K!&RR{+ zZ0%aD9lI}k!uCoER@G}6)kUn>*=pt@&Dj+W79hKGw<0%I7Y-Id+SUju)c**U-d>v-aBj(1j$*MZ=(W%a?{!}+Yrq`dh_urbPE_r-W3y0LouVHX+jI5#Dw_lYW$C>$r$r;`yGXheBPc*-8uq>v;qD@ivyHVPA zE7rd7`s=TNqRS)Y1{G}f7^oa4-evN(Ns!{?US)G-g>qOW^qOj!?fQ85^I@}&P zmXGta3o}LawilKYGJF@p`jG?$zt=sblR_DcOC1T@d0xEa@7Sq}zHr;Oi4Xq~8F>wU zez zysh5};mCsYi+fhJ@;e32i-48*$Z922x>&xU9pg}rAjAxBCa-_LC*7Ai2USzxFo>RD zt$WGLtI@vjVbOSS(Tj-fHgs;{IIKIJ&ph2fy!~WweVVSt?QT@Qm*o2Qci6MG>QWNt z?~&$OyjFAJ4xH>iND%owfwyV#zx z1$f-%J>)&l@f(bKM2pbUFA?P{Nh){rmyc43L(9zy&HG@oZNXleWwS<$mtk#I z4UIy>m;v+4DZArpWpSKIP0=3SoD}(x-0TrmiB`W`(WPymHNBLpGvmeE^VuQ`io9L6 zv$beq@{+go$ETTblxn6<*m{t%$Y36_uOq(Orb}dfB}p;j&SrLxrLj%#d*I4!v61pPElk8QQBbaZi5^Em@n0RpcB|^_dB-Qt2U3Y1eJAJR0XTAt|tZ7f#p;axFE}=WD3gY^cz@cNMNlC~oo0G`2jR zc~WX^qgT#DU80n9AJDBwb7dYWX*@vUegi|~)bhe>( z4Qc{S{$i<}?CeIf7PYY$W#L{7a?Xs=U}#3t;#hpyY9AI~Vz?+U8tdFVtt9_O zor_wn=Hd;d;~X4@Y)z};x{X{1X8;2q150fiQZZG0Y8x8Aj84&%0IYuC`7oYE)V<5X zBX{EMj-*iS8bMPg-v-`_(sy;D=KSber<@D=CkVX|9cO3ky zpb8(m3;OL1q*ZlFSs)|ir*5jIKr-{Y^|eaAb2p^agi>bHgeu7DPF_tu&5gJ0uUC^* zMYf}m+I%dh*T8+XCdmsQGaP$IV)`jH&wHuurC=Saflej%NX12NlH#`F^*z^k`lN~{|VV3f+3Y7_Q3TNz1gcQbEY$xYZw7qCIzsS6Rb zvuc45=K^K$uN*wg^vUWC8x_IZLCi z&cTaJ(%i&c>BXEYK>MK1oy73HGhtTKe~;$Tk2!ybN*LO+3@iP%(4tb!p5cDTbNuza z)lK!12>gD;qMLuX{cK9_Wj1$Ipms@A0ym4AFkZMLzsJ}B@?G|nwMsMrvCNM1oVS_H zT+;ACCH8`QNiL5_Jq_KF_ml;goRr=sV`onm=`au7wqFq*I=>i(Qr){yXCz?&c3FlOA@sMz^X z1s~oRD#Vv=)CB+zskp9i&-6vWJL=lm8s34owBzBKJE?`xKJT5a*-_6)AQD3<-LY zGN~pB#13A>{Ta)vFcG1~sl9gYC4O}G>UI5^Oi_7%L7%C-weGlnbBuuvHdlH38wU3B_I|*t zOGl9skF?hEXOR5%*9~9^yfQO5`ylQ@8x)KRY+7rzbd0*9*tioq6iW0ip zVLuVuGp4X?I~JP909rlW^M{943kth@91zN z84bquv&)g%dI1c`N5lSiVMDTvw6^^;!qDIkfZ5ub8GRnw0!65;j^#A)V889W&?$Y` zsudHw>w;+sG(1!K@@0}f5zfswsm6hrxk@_Hv44TDJEaOCRks5%2C2u+t6yhV2|+|v;NFfx1n_2cjKV-o=+&$ zMFw3u3c6g9lS;oiJm8Ci&Z{bXwl3kEkBI(wlYRWsJ?p!@tj_Io;$DDsGEAiLQ7!4x z9CACyMCk%b;QKCcE*0kOn6d*>1JdstkB_xFFP+ORF}E?jkWbTrdgFtH`zF|Nu$NWj zC!Jft+-aBEnA12KpVuT%FNi3(-^sP>G`zf%*lPyumr64I{{CIiOldy8S;93wCEroN z2ZO&mR_DE0|`)9{faK3|Ys6_7v%@>5?~&;ZV~@ zNaHx0Xq$3qah+iWc>~~_nl4e5PH;PVfahvO$1BwjIwMpv~y;YE#_?aWqe zV;L@|7#{7=cm%bKM_9l|e2*3gn{UnNROnk^89Qpt>B-qF6Fi?-mQ4%g6*n8s4in3X zU1iTLk{3OuByxwwP7PE+t(5x%%cpIjW6FYA%P!Ss&I7fj zBy@^o5`&3YS!>c^b^j|IzBj2%dmq1hq(OP{A?#~h zM&VEia^w)BXx2TXG-9KFcqmHR3@XxKQUJsgGeC&)epq@k(kv~bpfAnd)9`{kQ?&#T z;j4U6Pfnq*3PHmZ3-M9X{qY{EQa5Jhaol>Y%jBd6H9xDvgW6U?wNpYl!k-j(;G+2 zQW{&un$Qel0yh>6}>M2OM z&i48CI+tc8IedhVmln6lWCP@H_KoQ*|mQsa2b(C?si4|54Z|GV@Nd z|GiT<7&`lY;R^YLMZ?e|&ka+8C6{Y`mrq*x#twBVxE4GXpUbM+4PiQJ)w9Ct!_H|Y zPln zM&t{ed@qWo@DEh&YFrdpbP{4Icd>JG;oJdOU ze9SIF7uEX|+z>P=sMcz?km?JZh5hy)e2Kiol;Y}%TRp{-YEQtB8%yaD?*2VpaAJvS z{Hv+oiJ2qJpzDAIqe#M38Iae>;78jbFBX%U9E^?JMxS*7TeP&9LASn523^P9sE`c; znDoJ2R91Vm?POB1RDGqVqrF2BDSd_Y#Cb=4102hAcm~G8CWhY*T@!c$&6}<{Lz&2g z`7t^~EO#~fnnRk53C=+_{7eM;pIuvUtuXNXF2|S_ff%JW(QWE}wynnL5JdvKFk4G& z4)l5wo8%~!);2Q9#Ua92Rs9T_XJbbxgnR(vBa>uFrzDheN*eI=SrJa zF7<#RXUD@dG5W{65iQ=^c)~J^8VkN`Mhjxd5;HlQ9;?x$Yl_Q>*jte( zbss_bdJnqcVw3u{4yr%dGa|iz_%Y_$TUNmH$|7E5(8PGGPp9-afbjutIRbq=8+~+w zm;C0~_xnl4+1`Ns0c-gBICDh^Lyv-jF-sVfE}RDuU|6F;7j9z>?1M}(Xye`|gYcf5 zuQI`Us{ZrP`Vh$`(=K0azHy~0FgZo@+~x^jVt_OxrRk)8gpFxEaf84ybT`DGDJdv% zeoLwULBQ1+rcl8|c?C5ng-WQCo!!u)7m&AEppnyQajdF9U&CbI8B)A_kIt(b_1cVo z&S(|l4dkC}w<0!nDC?MiSZvG};?9Z+$SbfhYxp(l9=~2+h=UO=U1lxYt z2#@Q%SgLTQXLgw`u;tN3a1{? zeJda8JDgt{*o!FRn&2Z)}dT1>q~lGCw|4ObPF4Nb|&T^wt#$cl7Lvoi?f}#f;5k#b23+) za)8X)sZxDJfp2Q#?q*BN8uCYYVD>=iBpdEWprzzHOECr1O&EujmR7yVSy898(SRQA z1ocZo1`5OySna3_XTsxm0^`b7r#1DpV30vs_S_8j0%8m~S8CQ};jkAe1rz+~PTM@i zMEa$?|f&nb!FV!QKuxoh9&XXQ_^fLqk8r;4g67>gJXr3Y7m z?S28?YHcA;lJO^78$IRyHEIV)i@8i8`cazbFnc1Z=h5UL74c~dhj*EJ<^=n!yaJik zjO!6)=O&S+Rk3pc_%FbAo zr);I24sUKmYJfamuwe32;EvRL6EQF3IpvY*m&Pk^If!4UyS7U4&9f_N z;S;J5LMKPnT=;`!{tWJ<(uavYs1qUsS*W_T3108s+(hO!EK8DN*8_Eaf!rjQsc>xx z66e^VDQcBJmDRgnKYk9TuzIy8Ja>IL8K26h7ayHWLA?m~nx4VpGTprzB&UtN7R!hW zV(uKa3YUd*Ia;*GCC`G#D8ADaR!s&^mD|^EJ`(|B|CK0YY00-2oIuV6vpfMLz3lT> z`)r81-xyIHH~Qc`Mmny(2@IO*PeRE;EKbW$+8@bbJq-&%QzTRK6eY$B^>^4beg{uh za!V{mbuGVfsYXvg%Pa;Ty?d3mnbbd;C)MynoKF1xxN>`3xev-1nGB{iE19X7O(^b+9!NhOp){XJ>9oxt<2jwh5VEh=5Alc zVqVLu*Pm>!6^wImIr`V6RIy;*r3vH00Zw9Hfs*Q>Fq%{73d}%%3K6xE7>y?DMju|W z$>VpdguXTHc&4CUy#jU39Av-JN+09)lDpu0eA3;zJCOSVgQh&1MYvNbxkb z1jOev_XLMCEB?GE`R$Ar|0TMMeC%fcj3ub{ zyV~&?#}LfBiH=-q4AXZ%#!XX*+3|;GJwiwRlI@TfS_)E;Z3j&C+DwStNjjG{4)U}l z^=Tk^?`adFc>Ghd_-r~d&cH6eEAW~%(o>g0PhtByjQhh}j^b>3lcXZunSy?ai&a?yLdlCWwfFLe#;HB?p!8P8`DRJy2YQ!Q zS-6aK%pHB92u=TC#a7#TN@UW?(}WiV7*FD0VF{3)){<6z)(d9AXTVhdR!L8V=EFGe zZ4!K|S)Y)?=hMIDG++ru32Ej{Qwi@Z+s=l{gESH08s4x` zwyX_H4jheabl~@!{v`amwr=i83V7HP_rRCm8~5EDQD85&2SJc=@Er@1$iz>!-D8Ak zX4O5rX#910)hpoYlS>?5nHl@&FE8$%*a0aMoc&cT;hTuy_Abw4IPqyWuS2LkK@kZg zYz(J_qPgGTjr|u8{ryAQ7FdQKK1W^RMG`irytggO9IT49+7os*r%ED1mrF>I%W>$# zFwv^Kw3(rW#(u6-B~@1!S`7!BcdZOxHU(9;lGq>m!YQ%}nIEUGrAthR_opA@g!58e zo{mr;Zbz{po>|CPbEGLyFBk(MjSZD(-4o#mZalFerjZ2<@Q=BJ`t56ouZQCz?k>#J zo(pyFSeVty44b$dy)pkTNZk6a;}md0s1f(LnvO8jfvSTnJPuw*%v@4u`vI87;**oL z9!LVCZk~l@-|kRN4!ajw>jj z?W*~kOK5M!*c%F57zI?vD9-T{RotD1h_0v+D$wx+{XTLkW~K=^&fUoJ<#H+a2y@k? z@Xl^$Lj;Z#d7+I;6sqt=^?EqtdTW3-oiH2Z6ANuG&GNj6RFb5DM)#g@TMd|GX_FG! zpUGQobznt+b3KE>8SS`u`EtM+53kcoPEmhU-~wjE%F8Xw^Zrs$oD3w!E~E0)72e!G zo^>{TDOJ@o&3Q{T(W>4oI@M%|`Pn0?N6;j-ZUW#<6o3ae+DQ>;$`*^FILdyE?M?QK z%(Jb1;ruON9%JUgDRY;1EqVY2&Zj8x)YSiotdhxeYl< za5!Rq-!$Y@QXBtxx?Xa{Z^b7(e$;i?lUU;GfW)fGMN-D1(cR5fWTmIoD@yBt>}$R9 zg-;^3R{(Kk%}`@xKSSB#G>55EO1FT1TVJgT@ju;T=&TIE*a>WpW>uYx5e^)TcVEyu z8hiWOkE&BRcTUx@S1zAlyAO^9OPBkh?)Ju$Q+@%lG zklSCkET2=$y`!cl{EUt?FsCae!flOAI%mi%dUroZc`SW~Vmb+fX&*F349?zq-C8#g zZaQ#VE#l_P0xUE_@wPLLxusjN+GTIO7%^2%qtUaL`oc}&XS1?_BmJ&i&z+6o#m4QZ z7LVlmVcTioVBLmoggN8g&|0;K_ECCA8KbXZlApmm!8)!uo-)Q9>3WK#2~m=e!s1wR zM&-0(z4>y40=58sL=KGI`iEwe6iscZg1BkTUgyMG$R(E+g7|`?3~r{7SQX>KFcuiH z9XS5g)_J=%AGWiru0EBX3(g58JsmVAC>P*3u-n^?f4Z$a<0!fS;V@?fI6&+o#n~0I z3A;y9*FuiO7Xz_mCEd4t8m*490IKiu??Noa=(>#H!36e}?t1g< zyQ}uS`%omc+;O+bXmwW9!jlendDnV^G{ldY30}bCmuu@f#0+`iu{p+wJ1PD>aY3sG zhwq)HB1W18Or2<( z-c^LG9P2v4OT6D62Za8)P3`!t1<89jx)}Vc8=#Sl$eRY| zsgZC5!o% zoHZ@2uI!BRm5MlYi65=1WVZdWcAI#9h}v&~6_Md*ud?mI;StJt?e`{O)=4&2H zmVMLr_}J!T{cxuQztWKT<2lkA@mtAkCKhi~Cz1hxIN%W;TT*x8GZKP`yM|Kzuy%ea z)ISEQLz)hfDvQEoSrt&E$yW4FG(_~k_^qhIC~{pc#@f5OC%JUQT#G^(1D?H(#6po- zoCs9z-O7lt5=$3=_A#2^%_wX?;aDH3uEx3xV)2F2USe?w-TVlPFw!oB5LK_VF+lzdnO&QREI}6zYvC2;kg!>-7D}Q`Z z?}_2WS{)3pILy*2RgCHLJv8$`kDuUva)003T7li@@=s_y2MXeLxK zx&iaC_5CaVvyw5=`5Mf8Nq*$kk--ua9tB=P0_#VT6Y zY{22`^nN!tWmvz2l9u;T+`g-970I8w6MnzPLTs-G`>D`>hpYMC;SKR;r%<`D)nj|~ z06?xWS^(0v#+C6iJ-6$s?wOSRQ1b6j_-;{PGXf!l0KzH_h(3q9E(1qU{8G)9Kb@vy zflN#-!W7YWK)nEl6;BakUt$Gjhr1UY%1M)uDg2S2+pE@THTxV655)p--+5_bdd(;} zoJ&uAPo@9#EF<(7Y_6|{?DawS2Sr8kvIs%AK89-zSAS$L=6QZPL?r4jT(8RBpIUci zhRnlAX|^|a-=Vug0Ra2-SKO@*A_mH8Rshgxi`M>mqVw<4-F`1|>& z{7M z(2gm|@ShmKJ%#z#8(D9gTK}=yVZS@c|Jvd|HwVh@11bI9Y5lE~|NDaVu1sa!XOFHY z=-*2t(atA-3uyzIgf#=re9v1 z5A#7SA@pf~BRW7zrvF#Z)&WJ`6`I>3<^IJ@$HwlLJ=vdwms+NTE*%j}^TREt`HKhW zQ8}fyfqO>&EZm{|%Osva*R8m(ahG&Q*e>p1;&q11P5A}(bpwBs$$>w(NLzWY5e+x+ zr(o3KeQSg`TN@&-fzPwYviwue|3@!$F%*CioxW&O_#gH9pPGcf#wR3{6o4Z6x>hB4>L^XvjA?Kzq{J$Y|Vo1K!_t8|Z)KH(CJN z{B1*J{vfu}_%KbX0IpPf4>|VBY0~uoxelo`skv+AI_?Lt@j8Hw$Jg6T9AJQ7SWM;P zz7Rb7t{hU@aA>Ec7fePbZ!0!&H@-}ezWe$ZG=G)(&ckd@)tAu-t3gj#6LaCc8OuyiEke-Bg zo6L;n52LC-L$Lq3uD}B-{M|Au|G5&f**-=o5BU+LobTkq+suKE!L|*;6 z{hpL6X?#_;R@1C!zEy0(tFN&Ba*RLi(IeWmwtVmR$=muFkK9`Xf3HqJPXs8pOCH`4 z5;9BM19<&zy{{kRDjtOx#jcPqUpUB;l%@oM=+mR)jP&sbuvN!st^=z<7X1@U`O9&) zo;mH<{^rM@2N0;~X<$M6-HZx2K-3-EGQ88L}48I%MTFFR5SAlB@=W{{9>+uepN{c&N)T7`G=oCsb8qo zrIgB07R59cAP*O;9q63YeE=VkDa-A!HgF7Oogc6@7ts~GgWUQysOWy6l`ZUhwWs8c zQ2uPRO`zSggGAUMx#Wqi8Nu56BH(9k7!-&r;A)uxVMP)#jL7uyi~o5$`-dY7;Kjqew_{r6Xw48ho! zLgmO5Kikh$@(b5NhBBG&*D~85&m~R3AwQ^?L3VdBg9<(MK?dhX{e9Hjk<TgN@>qR9W5FY!t@gE?nGyQZpoZBEd z07WHzWDbO1{6^_aZvuM~%@>%(FI%>L6pQ#-3t<0&z|&-t4F3-h6I62w@qJ z^sNI)x(GC@l(96;umdCk04%%GgMSt6e`_h<`^p#RIf3WCfbCIZe%%@0G{6;kcc9gG zlN=KJTS4y&7ZNLa0|W`?m$>l&rzG z!(E=0e@G$<WW<_{?q30qO@y*Q9K{+i{D~myJs-co+(AEU27tG%ah!Ry zi8a|uTL*#l!ew4cJ}UqOPQtDa6E~JTU%C%_{O?7P^&PZt#@u26u35*trC`9R;R^gH zu5`J624p$FtLZFh@~^rAWUDmrXz-*|6yOcOduKC0WS&%yf>)ohSOdV8MrG?=xJ)U4 z93GUlGV;;`K=Qr^$z~rf@&Ws41_kGAX~+5~YxC)~gdI|XHF3~2;no%il=7poEX{tQ zD(~~{S^zzNcv%sMJe~(=B?N-wyH~=O8^kmItgJs*1n@@0#sDv+rwm{~s>UONEgd>FvjGRLp?+gB8b6hI;^Wpyw6k zCP1#sLayc}7#(e-cW%5D_M+hRCGOj%-zZ~UCwDl#0c5|_yPA{$sg*Zy)6bjU>*k(+ zAP;SK!7o4#se&WZD0w-aZ$Se%sKyysmf}nv|qUf*G|{HpY+G_7C^m;brnA& zxU-Ji(f0(f#_8MB3ENi9v$+%$lowI}ovq(6Zjo3lodLNsd8ce=znJ#X7&q#Te5O@R z59qHGy8o-37#|}Aow{W{Np~r6w(+z$SjS+a*(=z6;9VaG{SNP|0Ol+WB0zH6k4(!LVuB@A6ks*Ts1AYt(Qhd7rqMyVyciVr4fx0&- zfbs%&-~c&K3qUh1V!;{$TK<*g0GIpz;Bt6jI=|p5SViDjPU<0td-cp33{lI~z;bdK znP`+Zg;8$;KyYk1aHp%(D#)QtT2c)#8)w7MM;i|a(AYph@D&`GiSp7FQ43$jt?%~; zmqC6=Fsw<(#1pJU>%SOIwRgmW0n22y~rD@oZ#~C$E?XtNi)tEU|kSGm`EAPa(*DwL*Kbc zC?=OTdJ2T8z^9WUf=Z?9dWNS}T+=q2u^h4&vP*%A5;Mai00~YsyR3PtsHlxXrF?UO zmv};0Z1YDI7G$}WpB4$;1fo|h01g+C<_eIsyFeJnqyYhsZ;`G*)+rjR!`$K?w1f28 z8QD|FR=$^wks^s;od;|`!1oduvQYs_1bE#C@lj1s0v=?SlmK#*HbWBs{DOQtJEV%E z3N$c%6X#l8=8aunS0_V!e-oe$0QYx9>~uogVle0j0jmHnDE>oipwe-B)U|Cu9(pWa zbGXY!PdL2icR~L%^W+(NM9?>t-*CTw%gcfgEIJ`ICjY}PI?m^At;I8~;B??aQzLh)4E54)#)^qp!#3|3^UHlXgkUH2f(8BK=cxC&5BNAP!@*4D$-jk$j^U)B&d$? zVCU{FkR7CNmwzRr&$UmOyYr%$aK+Lq2PKILv>i8W;i93VEWAcyD7z1U78x8mX0dSs zO=y}jd;03EWGGsEOm_B_GyN{RwIi8j#Jt^WN;!-2ukr;r4!aG~qr{56;>}oZ9WS;} z)-So=EGb5Qv{XlY^iSH|gYygrUZaw{KIaw)cF=XWOdPBtfKru0RC`d#7ytM~jr}k< zZ!a)3D|C&UtNPYvC4h1?nZTJSVE-!cLFL_LtPx(>uw6V8spSzns9E< z5exHUKdqYN1x>xZ8N9O=Y`wEZOt=r#np^_bGZjGO789Tl1Ywxz>A^%X$_szW%rL7X zbla&-);?KRY^`$g>a3e_+-F>?JgTxWpg9tZy%%c5Y5bhmiCZbLx~F_Ke+GoD8`b1< zXe&(8N39oPIy;gcdZ}xaciadPeQSNhhuIBFIWA}f^1Q8ljhcuh36@uze6W-Y<=@WZ z-d`DWZHeiLlD;90dA)68vY{`_W+yMAq?lEw1K2*R#cqhez-dfJBQ6st=EPuk1Co#@ zL}PTU!go9cMO5kYz&BxmJq+IGg}5b6LG{h^BegMKhEC2TTT0U=?MoU<4r+U?Q=N zlmh2V8t$51+~|`Mb6GP0GCW3`Pt6u237m3K=Em+?zsHRkQ3Ye|}`nQ2yl z!NUcN-Rw{BBn<;B+7jYRO3QHL%H_zBhI>-WuLY1K-iFMy;OPhN3O&oE^i*OQG&uT#sC6;(jZ2!A)A1jE%7!tK|?-vN~5 z;53O#E{R!`>%{GgihZ`ZO~_ULNKf96>QOlE$aF#bILt7Dv>>4Q9pL%Qb+7sJt1{4% zt%A&fb&&1=LWi0PFU;e`9c^HXMv3n5r3CH1s@tjIi6}s=T}Go`B2+TlU3@ohI-wG7 zmb&1>s8v-8=nAe6dL$_puJEC5Nm(kg3Dl^HL= zGk#*JNy~yp*%ls+Ak?x8VDx_|nTq6!fm@a}y>Ckw8Ekv?!M)&II7Xt%P}3>9PB##t zMAV!%7C2K28g4AGQF`4lszgP#tam)qDp^$Gr7v!4+O{y^pVTRT*{h#sbApcXlZcj#e#dntwbjk-Aui)@fD zy@?gH^@(Il;RB@Ut^-TS!K@%$qf#K`P1>4pn+wxZj&N>T0-&RqIZFn-m)ON|I9{}A z@r~X4JQ_s!kZN>lzu+PZEqV;7(@>SfmO7_eV%O|&b8L8$x1wi(=fvzY3lb45)KX9< zp36w$ahjWq95f4th#S#Q;+^8{lIe%>UFWUJgIChKY@Y*_PEPt6>P#({MYLGED|Nc* zzx=}46B8@#yy=kj@d&}y5%fmthi_Vy<6uo@xjm~B#4{f(tPL{f>Enhqor~TZ9#tUs z$b5!aY;y2&cJ7LsT53cHRwR9kwt)pL8j36%{55+MkvOMO!1DTQ@kz@qu_ySriS>A! z>Q`;+tQ90f)VH|#%QX(j@|Ki1URx#AYD8nO6)N3--wRfwW==5CADxW11%Rt@_mj~c zPW5Nh3qsug#Gr+kRkat!W}Z{8m~c?CP+*_RloW#=P>S75+K($S1s|z@9PT;1mc-Sn ztbkCdABbU}U}rIMX}bd#b7be-2KFKo;+ocw2~DtHAVt=tRRMwx=!qO13R}55s5Jd0 zMMunivf&3Fni{hXL;&}td3V$^*MN<DdnX9_MD}3KV7vHY~dyQ2=hc~*$lfngW-&e`l+v;Je*#LSoAQ$IYAmI zrO`hiO{d&2AE^O~hx$u9NmDV^uz2RG0|hG12cg}M&o6n>)TqPqzkFaXLi zLtEbtF|Nr#ra>ABp{NFux%g_wQVJgha+k*Pw9q_6^#^dE=D`z{Mp*J$avkG{8lVPK zM;AyCSazx&1Ihj+?Gk2Y6YkzC?-(Iw@tSDMz`EjO%^A$SKn+z$e&*L`JI~_NF!%Ts z3|rUv@3~D0#0r2Ft;y@P^wPll)L?_=aKe`XglkHKB~Yo<5r=svL}Cy}uCCwkWd&%bV3wi;_UymFR2|Ra*U*4FJt)cl9#YN9{oX1Yi{@6XCMc!+Tv~Dwuwj z?kxAz-~xK#lQCd`Tvg*&*X0=3)xs)53;L|F92N5z%JjUgOjUq`Ll+ez+;L8S@>957 zt?Ll)Rj1l(U9|B({`B?2u}d~dS=!bZPXo}?)DM@H|C|N=K)Qs4mgsqrE}KC=CDlY( zgPD_^)A5${!|p_J4bxb>Wv0%cukRp!65h-r=A8r^V|r%FIjgVT{Kl@|Fqx$lHy_#L zaIGgbw6D2MY>_pQdA1gwG3rwg>BX-q z;Q;>Q?3)t;Ce_n%i|CPP!vqUF-K0pIU3?3Ex=49)ERvBZtiKQI;_Ibo^6=JwxoT61 zUdC#^V6GKsmt~`BU5rq_i}DF;FG?LQwOdSTQ(xBf3$hz72zrn_k(<+0$9pRJqC=+i zt3Pw*Zjz5T5ok6f!xF6ALg!UU9fnfUL?U~D!pM5oY~2&)TFJ8d#%?m~u#|wiA*=4%^KyHYDpvqCLHvN1WrYz&y9SsywBTA4|_Scp=t{ z$KO~Q5Dm{fOjyshP^dZ1j#8;_RjeZ>+rP(v1QmGMg5h#5%*ZZSEZK>tzF=ZKX0yM( z(MGV4ed#Jpu|v-L|hw9PT}M_gi_xc53SY~eeu0OnkUs3bqCiKjase!MKu zKrB$pKJ9GL>tmHJ4&A)Y=$MAv<-(kU{j2$6+~=1`5`b4c8-Ff(k>DN=Q$HhRP)`gS zHod;mKCfRJvGeiFiyvEKNeB65iITb$G=iLDjOq`N6P>Qq5-8hP~Hb!ppSl zjQl79TFe&7Jt4)yU5Y|Z5ZbGZ^iQ&_T|XQNO^t=h+UuW5mdTH5e+Rr#;m|WK6a%G( zY(k6t?DERpC^UbJ7bF}rsuD#6B2qP@Tag#OQ~5V?Q0c-c0?7rehwXxw2adu*DByWQ6Ks(A#q1A z%|>_xWKpN3K<2Hwrc)A(7oCe$ZNgu}%2IzcNKS0!Y)e{P6)Vp%DpL=_0);5b;0QBU zF&OfVa+DkLvE9(U6t+e|GA?#lHQgsEe4eL6>ZLm2NjGb+yK@OZEu~JF3(TmghTab# z!F`y`2^(MxN6*)J8X~jo!ab8&yf1e&u_OhuhNZK6a}T>02*|l5&9QE@4+IFlP-^Lf z4%R5Fa+mQ4X*AzpcehJcAN|^rFXru~!G2WPtlU;nEL=df+0!Z{51@|Nppp z&v3T?u>HT(sNJHqNA10dphipWQ5r4Pn4vLiOA*Q!ReP@ptyOB&-mA6)=t(jh3D+Qzrz+T4Q|VisiLDJ=6NK9A zjf?ETWPS93OpUye9z023w0kjN!D9zoPCM3Bq8rtOYYyRo7H1x`&E0O9sm-b@5AiDD zySld3Exl_YW0>MOI}L-H{qe7^sQ;-?;v@^su5* zth2o1|59w`3*waf@SW}ywgB;rsW@ZBV_dal)waV>L%ueFX_{E3yjbXy2T+EuJ;D3T3lI;KU!LtJ5eFzY+Y=gd*(FrygC5mb~u0Z%2;rIkMLsINAQ==aYrElIFb^PsU;JWo=v|-CJ;+J_h zXP=Emy*r#K@`_@#&1ZicCPflHvO@e}ZjP)M2c9|;YcHa0eoM9h;)|9f`qaAbn@Pc2Z^Y;N*qwa-!|mT02WdSjPO&^KDt;PW_5(&_ zr}Gni|Kv~P-+x|_BY0mMI;%9|;V8PcEBMZe*kf2e6MUpH3wp`TG?vcfu#hZtnQ5rwE!D6T#b71G*16b1 zYeBG47dI(sMqGm+qTVd)za8N16~cRDa~$vIZEGPg@`iFoRDW2TK;yhSJP@R|51^d^ z;lV`+b^7O9zmn;*o(YCU>-U9R$?W8NEc-CcJg}{?3n%22r+G^UWq*{&Q6ud_;UBE#QgH}#Sh8Bc=za4$AoEcP^Y7`$E?rOS8Wo=!LOl2V zeZ$h;*O=>PzK^K8svE~;6Y9}cfq@3x3JZw)))upi57Z#k(@d{Y_Pk0%;Og>WBf-L& zrB?f?p>7msNApcvD(wYjr%6wk5zE=x1_q9s6=+*J#+hCs%0)5(x<0 zDG}jP@9Nf)dca4*m{X=IXvRWEGeLp3Z-tFDHpF4vZjc-cWCnEFo1m4ZbuNqrhj?^)DXyuhf)8$dr2YidlK> zcjB9g%%T}Of`MZ{r##vN-2p_J+fSc#UV9@k5m31>bM;fj8kjNHJF^JJs}qwSR5sgJ zh>ik-k`Pm>2-mKany;TKn^zobkhS&sRGIoZAku+cF#7#xcSGVy09@*BH9~n$E^Zl# z2VC07+nS3VS(s!VG)QhPW~`XAOA0ANzpWyf(vxxctrM5a65%y9Qys^MSGnf}+vxPi zLIlIUiH7Bgv$oUbv4Z1t|INRqqd(on~XS zPu$b1>QNHm+EK6KBb)D)wJ4aU7gh{=rPWjRs>-@_LKvCi}M< z$|V~|9|>%8D&EK(&c8I%7Ggs6->9OrDC{lxJQ-&30XcDCVigE?+a6F^2_#L<-5fX0 z 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ƁV#TNd6!rO+NX-6s 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%0DUH^*dbH}0cyA2jk1v8JlMMnT-^pE|UT z;za>058youmJHRbT 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∋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

b~ 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

r_wPv*hzhcikD-o+tKZA837osVNuP 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

B+fa&RdJm9FM(^)$`X)V#zHD5&sg)#? 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 z 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

?y?um-V;HGVaL85fjY$Zxr|t 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 `quarkus The legacy configurations are now deprecated but will still work during a transition period. ==== +=== Disable all or parts of the OpenTelemetry extension + +Once you add the dependency, the extension will be enabled by default but there are a few ways to disable the OpenTelemetry extension globally or partially. + +|=== +|Property name |Default value |Description + +|`quarkus.otel.enabled` +|true +|If false, disable the OpenTelemetry usage at *build* time. + +|`quarkus.otel.sdk.disabled` +|false +|Comes from the OpenTelemetry autoconfiguration. If true, will disable the OpenTelemetry SDK usage at *runtime*. + +|`quarkus.otel.traces.enabled` +|true +|If false, disable the OpenTelemetry tracing usage at *build* time. + +|`quarkus.otel.exporter.otlp.enabled` +|true +|If false will disable the default OTLP exporter at *build* time. +|=== + +If you need to enable or disable the exporter at runtime, you can use the <> because it has the ability to filter out all the spans if needed. + + == Run the application The first step is to configure and start the https://opentelemetry.io/docs/collector/[OpenTelemetry Collector] to receive, process and export telemetry data to https://www.jaegertracing.io/[Jaeger] that will display the captured traces. @@ -359,6 +385,7 @@ public class CustomConfiguration { By setting `quarkus.otel.traces.eusp.enabled=true` you can add information about the user related to each span. The user's ID and roles will be added to the span attributes, if available. +[[sampler]] === Sampler A https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling[sampler] decides whether a trace should be discarded or forwarded, effectively managing noise and reducing overhead by limiting the number of collected traces sent to the collector. diff --git a/docs/src/main/asciidoc/opentracing.adoc b/docs/src/main/asciidoc/opentracing.adoc index 88fb99d143a77e..c089dc6847c940 100644 --- a/docs/src/main/asciidoc/opentracing.adoc +++ b/docs/src/main/asciidoc/opentracing.adoc @@ -16,7 +16,7 @@ interactive web applications. [IMPORTANT] ==== -xref:opentelemetry.adoc[OpenTelemetry] is the recommended approach to tracing and telemetry for Quarkus. +xref:opentelemetry.adoc[OpenTelemetry] is the recommended approach to tracing and telemetry for Quarkus and xref:telemetry-opentracing-to-otel-tutorial.adoc[a guide to help with the migration] is available. When Quarkus will upgrade to Eclipse MicroProfile 6, the SmallRye OpenTracing support will be discontinued. ==== diff --git a/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc b/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc new file mode 100644 index 00000000000000..63348b95ea7f13 --- /dev/null +++ b/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc @@ -0,0 +1,544 @@ +//// +This tutorial is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// +[id="telemetry-opentracing-to-otel-tutorial"] += Migrate from OpenTracing to OpenTelemetry tracing +:categories: observability +:diataxis-type: tutorial +include::_attributes.adoc[] +:topics: observability,opentracing,opentelemetry,tracing,migration +:extensions: io.quarkus:quarkus-smallrye-opentracing,io.quarkus:quarkus-opentelemetry + +Migrate an application from xref:opentracing.adoc[OpenTracing] to xref:opentelemetry.adoc[OpenTelemetry tracing] in Quarkus 3.x. + +The legacy OpenTracing framework has been deprecated in favor of the new OpenTelemetry tracing framework. We announced the https://quarkus.io/blog/quarkus-observability-roadmap-2023/#opentracing-archived[OpenTracing deprecation on November 2022], and we are dropping the extension from Quarkus core repository and moving it to the Quarkiverse Hub. + +It is now time to migrate your application to OpenTelemetry tracing if you haven’t done it yet. + +If you need to migrate from Quarkus 2.16.x please beware that configuration properties are different and you should check the older Quarkus OpenTelemetry guide version, https://quarkus.io/version/2.16/guides/opentelemetry#configuration-reference[here]. + +== Prerequisites + +include::{includes}/prerequisites.adoc[] + +== Summary + +The demo has 5 parts. Please read the summary and then jump to the section that best fits your use case. + +1 - The *starting point* presents the quickstart app that uses OpenTracing + +2 - The first part is good for anyone performing a *big bang change* of OpenTracing when you don't have any manual instrumentation + +3 - This is the *big bang replacement* of OpenTracing when you have manually instrumented the code. We explain the main differences between OpenTracing and OpenTelemetry + +4 - The last part uses the *OpenTracing shim*. This is useful if you have a large application with manually instrumented code. It can help performing the migration step by step because it allows the use of the legacy OpenTracing API on top of new OpenTelemetry API + +5 - Conclusion and additional resources + +The tasks described below fall into 3 categories: + +* Dependencies +* Configuration +* Code + +[[starting-point]] +== Starting point + +This tutorial is built on top of the `opentracing-quickstart` legacy project. + +=== Generate the legacy project + +Create the legacy project by executing the following command: + +:create-app-artifact-id: opentracing-quickstart +:create-app-extensions: resteasy-reactive,quarkus-smallrye-opentracing +:create-app-code: +include::{includes}/devtools/create-app.adoc[] + +This command generates the Maven structure importing the `smallrye-opentracing` extension, which +includes the OpenTracing support and the default https://www.jaegertracing.io/[Jaeger] tracer. + +=== Check out the existing legacy project + +For convenience there is a project in github with all the steps from the tutorial. You can clone it with the following command: + +[source,bash] +---- +git clone git@github.com:quarkusio/opentracing-quickstart-migration.git +---- + +For convenience, https://github.com/quarkusio/opentracing-quickstart-migration[the repository] containing the app to migrate, includes several branches with commits mimicking the migration steps described in this tutorial. You can check out the `main` branch to start from the beginning. + +=== The application + +The Quarkus project has a single endpoint and the related class looks like this: + +[source,java] +---- +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello from RESTEasy Reactive"; + } +} +---- + +There is no OpenTracing specific code in the generated project, but the `smallrye-opentracing` extension is present and enabled by default, and it will automatically instrument the code. + +Let's start the Jaeger-all-in-one Docker image, where we will retrieve and see the captured traces: + +[source,bash] +---- +docker run -e COLLECTOR_OTLP_ENABLED=true -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 4317:4317 -p 4318:4318 -p 14250:14250 -p 14268:14268 -p 14269:14269 -p 9411:9411 jaegertracing/all-in-one:latest +---- + +At this point you can run the application with Quarkus dev mode: + +include::{includes}/devtools/dev.adoc[] + +If you call the http://localhost:8080/hello[`/hello` endpoint] the related traces can be retrieved in the Jaeger UI at this address: http://localhost:16686/ + +They will look like this: + +image::ot-to-otel-1.png[alt=OpenTracing span,role="center"] + +== Big bang change from OpenTracing to OpenTelemetry + +This is the happiest path, in this case there is no manual instrumentation. We can do a big bang change from OpenTracing to OpenTelemetry without side effects. + +=== Change dependencies + +To migrate between the two frameworks, you must drop the old `quarkus-smallrye-opentracing` extension and replace it by the `quarkus-opentelemetry` extension in the build file: + +The legacy extension is removed from the project: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-smallrye-opentracing + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-smallrye-opentracing") +---- + +The new one is added: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-opentelemetry + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-opentelemetry") +---- + +=== Application properties + +You should remove the old OpenTracing properties, starting with `quarkus.jaeger.*` from the `application.properties` file, like in this example: + +[source,application.properties] +---- +#Legacy OpenTracing properties to be removed +quarkus.jaeger.service-name=legume +quarkus.jaeger.sampler-type=const +quarkus.jaeger.sampler-param=1 +quarkus.jaeger.endpoint=http://localhost:14268/api/traces +quarkus.jaeger.log-trace-context=true +---- + +If you use the default values in the OpenTelemetry properties, there is no necessity to include anything in the `application.properties` file. + +Some common properties to migrate are: + +|=== +|Legacy OpenTracing property | New OpenTelemetry property + +|`quarkus.jaeger.service-name=legume` +|`quarkus.application.name=legume` + +|`quarkus.jaeger.endpoint=http://localhost:14268/api/traces` +|`quarkus.otel.exporter.otlp.traces.endpoint=http://localhost:4317` + +|`quarkus.jaeger.auth-token` +|`quarkus.otel.exporter.otlp.traces.headers` + +|`quarkus.jaeger.sampler-type` +|`quarkus.otel.traces.sampler` + +|`quarkus.jaeger.sampler-param` +|`quarkus.otel.traces.sampler.arg` + +|`quarkus.jaeger.tags` +|`quarkus.otel.resource.attributes` + +|`quarkus.jaeger.propagation` +|`quarkus.otel.propagators` +|=== + +The way the extensions can be enabled and disabled is very different. The OpenTelemetry extension is enabled by default and you can disable all or parts of it by checking xref:opentelemetry.adoc#disable-all-or-parts-of-the-opentelemetry-extension[this section of the OpenTelemetry guide]. + +All the OpenTelemetry properties and their defaults can be found in the xref:opentelemetry.adoc#configuration-reference[OpenTelemetry configuration reference]. + +=== Run the application + +Restarting Quarkus is not needed, auto-reload should have kicked in and you now can call the http://localhost:8080/hello[`/hello` endpoint] and then see the traces in the Jaeger UI: http://localhost:16686/ + +However, you can now see spans produced by the OpenTelemetry's auto-instrumentation instead of the OpenTracing one: + +image::ot-to-otel-2.png[alt=OpenTelemetry span,role="center"] + +If you don't have any manual instrumentation of your own, you are done! + +== The big bang replacement, when you have manual instrumentation + +Let's say instead of the `GreetingResource` class from above, you have something more complex. You will need additional work on top of the changes from the <>. + +This class now uses the `@Traced` annotation and creates a "manual" programmatic span. + +Copy/paste that code for the `GreetingResource` class in the quickstart project: + +[[greeting-resource-starting-point]] +=== The GreetingsResource with OpenTracing manual instrumentation + +[source,java] +---- +package org.acme; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.opentracing.Traced; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentracing.Tracer legacyTracer; <1> + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Traced(operationName = "Not needed, will change the current span name") <2> + public String hello() { + // Add a tag to the active span + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); <3> + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); <4> + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +<1> The legacy OpenTracing tracer, must be replaced by the new OpenTelemetry tracer. +<2> The `@Traced` annotation is replaced by the `@WithSpan` annotation but beware that this new annotation will always create a new Span. You shouldn't use it on JAX-RS endpoints because they are already instrumented. +<3> The `Tag` class is replaced by the `Attribute` class. `Tags` is replaced by the `SemanticAttributes` class, which should be used whenever possible, to keep attribute names consistent with the specification. +<4> There are new methods to handle errors in OpenTelemetry. + +The OpenTelemetry tracer is not compatible with the OpenTracing API. The main changes are summarized in the following table: + +|=== +|Note |MicroProfile OpenTracing v3 |OpenTelemetry + +|1 +|`@Inject io.opentracing.Tracer legacyTracer;` +|`@Injectio.opentelemetry.api.trace.Tracer otelTracer;` + +|2 +|`@Traced` +|`@WithSpan` + +|3 +|Tag +|Attribute + +|3 +|Tags +|SemanticAttributes + +|4 +|```innerSpan.setTag("error", true); +innerSpan.setTag("error.message", e.getMessage());``` +|```innerSpan.setStatus(ERROR); +innerSpan.recordException(e);``` + +|- +|Baggage carried by SpanContext in the Span | Baggage is an independent signal propagated in parallel with the OTel Context +|Baggage is an independent signal propagated in parallel with the OTel Context, it's not part of it. +|=== + +Once the dependencies have been updated, the above class will break the build because the quickstart project is now running with OpenTelemetry. Errors like this will show up in the logs: + +[source,bash] +---- +2023-10-27 16:11:12,454 ERROR [io.qua.dep.dev.IsolatedDevModeMain] (main) Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors + [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type io.opentracing.Tracer and qualifiers [@Default] +... +---- + +The new OpenTelemetry API must be used instead. This is one way to migrate the code: + +=== GreetingsResource with OpenTelemetry manual instrumentation + +[source,java] +---- +package org.acme; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import static io.opentelemetry.api.trace.StatusCode.*; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentelemetry.api.trace.Tracer otelTracer; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @WithSpan(value = "Not needed, will create a new span, child of the automatic JAX-RS span") + public String hello() { + // Add a tag to the active span + Span incomingSpan = Span.current(); + incomingSpan.setAttribute(SemanticAttributes.CODE_NAMESPACE, "GreetingResource"); + + // Create a manual inner span + Span innerSpan = otelTracer.spanBuilder("Count response chars").startSpan(); + try (Scope scope = innerSpan.makeCurrent()) { + final String response = "Hello from RESTEasy Reactive"; + innerSpan.setAttribute("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setStatus(ERROR); + innerSpan.recordException(e); + throw e; + } finally { + innerSpan.end(); + } + } +} + +---- + +Once you remove all the OpenTracing dependencies the code will build. Don't forget to double check if the traces contain the right spans. You can see them in the Jaeger UI: http://localhost:16686/. + +== The OpenTracing shim + +In this section, we present an OpenTelemetry library that can smooth the transition by providing access to the legacy OpenTracing API. This can help with the migration of large applications with many manual instrumentation points. + +To proceed with this section, the code project must be its <>. If you have changes related to the previous sections, please revert them or re-generate the project according to the <> instructions before proceeding. + +=== The dependencies + +Remove the `quarkus-smallrye-opentracing` extension and add the `quarkus-opentelemetry` extension and the `opentelemetry-opentracing-shim` library to the build file: + +The legacy extension is removed from the project: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-smallrye-opentracing + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-smallrye-opentracing") +---- + +The new one is added: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-opentelemetry + + + io.opentelemetry + opentelemetry-opentracing-shim + + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-opentelemetry") +implementation("io.quarkus:opentelemetry-opentracing-shim") +---- + +=== The code changes + +Remembering the initial version of the `GreetingResource` class from the <>: +[source, java] +---- +package org.acme; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.opentracing.Traced; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentracing.Tracer legacyTracer; <1> + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Traced(operationName = "Not needed, will change the current span name") <2> + public String hello() { + // Add a tag to the active span + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); <3> + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +<1> The `Tracer` annotation must be removed and instead, we need to inject the OpenTelemetry SDK. We will need it in <3>. +<2> The `@Traced` annotation is replaced by the `@WithSpan` annotation but beware that this new annotation will always create a new Span. You shouldn't use it on JAX-RS endpoints and we only have it here for demonstration purposes. +<3> We must obtain an instance of the `legacyTracer`. The Shim includes a utility class for this purpose: `Tracer legacyTracer = OpenTracingShim.createTracerShim(openTelemetry);` + +After the changes, the code will compile and you will be able to use both the OpenTracing and OpenTelemetry APIs at the same time: + +[source,java] +---- +package org.acme; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentelemetry.api.OpenTelemetry openTelemetry; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @WithSpan(value = "Not needed, will create a new span, child of the automatic JAX-RS span") + public String hello() { + // Add a tag to the active span + Tracer legacyTracer = OpenTracingShim.createTracerShim(openTelemetry); + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +[IMPORTANT] +==== +It's advised not to utilize the shim for a permanent solution but solely as a tool to smooth the migration. +==== + +== Conclusion and additional resources + +This tutorial showed how to migrate an application from OpenTracing to OpenTelemetry tracing in Quarkus 3.x. + +You can find more information about the migration to OpenTelemetry at: + +* https://github.com/quarkusio/opentracing-quickstart-migration[The companion GitHub repository for this tutorial] +* https://opentelemetry.io/docs/migration/opentracing/[Migrating from OpenTracing] +* https://opentelemetry.io/docs/specs/otel/compatibility/opentracing/[OpenTracing compatibility with OpenTelemetry] From 5be91d9325ab3895f86f09064f2bb6793217aca3 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Thu, 9 Nov 2023 19:34:22 +1100 Subject: [PATCH 20/68] Dev UI Fix Reactive messaging screen Signed-off-by: Phillip Kruger --- ...wc-smallrye-reactive-messaging-channels.js | 18 +++-- .../devui/DevReactiveMessagingInfos.java | 65 +++++++++---------- .../ReactiveMessagingJsonRpcService.java | 2 +- .../DevUIReactiveMessagingJsonRPCTest.java | 6 +- 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js b/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js index f382cbdf19ce2a..2f7abd6e6fcd58 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js @@ -61,7 +61,7 @@ export class QwcSmallryeReactiveMessagingChannels extends LitElement { > @@ -95,9 +95,19 @@ export class QwcSmallryeReactiveMessagingChannels extends LitElement { } _channelPublisherRenderer(channel) { - const publisher = channel.publisher; - if (publisher) { - return this._renderComponent(publisher); + const publishers = channel.publishers; + if (publishers) { + if (publishers.length === 1) { + return this._renderComponent(publishers[0]); + } else if (publishers.length > 1) { + return html` +

+ `; + } else { + return html`No publishers` + } } } diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java index 6f79ad41b86e17..3fc7884e69be0a 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java @@ -34,21 +34,24 @@ public List get() { .get(); // collect all channels - Map publishers = new HashMap<>(); + Map> publishers = new HashMap<>(); Map> consumers = new HashMap<>(); Function> fun = e -> new ArrayList<>(); // Unfortunately, there is no easy way to obtain the connectors metadata Connectors connectors = container.instance(Connectors.class).get(); - publishers.putAll(connectors.outgoingConnectors); + for (Entry entry : connectors.outgoingConnectors.entrySet()) { + publishers.computeIfAbsent(entry.getKey(), fun) + .add(entry.getValue()); + } for (Entry entry : connectors.incomingConnectors.entrySet()) { consumers.computeIfAbsent(entry.getKey(), fun) .add(entry.getValue()); } for (EmitterConfiguration emitter : context.getEmitterConfigurations()) { - publishers.put(emitter.name(), - new Component(ComponentType.EMITTER, + publishers.computeIfAbsent(emitter.name(), fun) + .add(new Component(ComponentType.EMITTER, emitter.broadcast() ? "@Broadcast " : "" + asCode(DevConsoleRecorder.EMITTERS.get(emitter.name())))); } @@ -58,23 +61,27 @@ public List get() { asCode(DevConsoleRecorder.CHANNELS.get(channel.channelName)))); } for (MediatorConfiguration mediator : context.getMediatorConfigurations()) { - boolean isProcessor = !mediator.getIncoming().isEmpty() && mediator.getOutgoing() != null; + boolean isProcessor = !mediator.getIncoming().isEmpty() && !mediator.getOutgoings().isEmpty(); if (isProcessor) { - publishers.put(mediator.getOutgoing(), - new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); + for (String outgoing : mediator.getOutgoings()) { + publishers.computeIfAbsent(outgoing, fun) + .add(new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); + } for (String incoming : mediator.getIncoming()) { consumers.computeIfAbsent(incoming, fun) .add(new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); } - } else if (mediator.getOutgoing() != null) { - StringBuilder builder = new StringBuilder(); - builder.append(asMethod(mediator.methodAsString())); - if (mediator.getBroadcast()) { - builder.append("[broadcast: true]"); + } else if (!mediator.getOutgoings().isEmpty()) { + for (String outgoing : mediator.getOutgoings()) { + StringBuilder builder = new StringBuilder(); + builder.append(asMethod(mediator.methodAsString())); + if (mediator.getBroadcast()) { + builder.append("[broadcast: true]"); + } + publishers.computeIfAbsent(outgoing, fun) + .add(new Component(ComponentType.PUBLISHER, builder.toString())); } - publishers.put(mediator.getOutgoing(), - new Component(ComponentType.PUBLISHER, builder.toString())); } else if (!mediator.getIncoming().isEmpty()) { for (String incoming : mediator.getIncoming()) { consumers.computeIfAbsent(incoming, fun) @@ -113,12 +120,12 @@ public List getChannels() { public static class DevChannelInfo implements Comparable { private final String name; - private final Component publisher; + private final List publishers; private final List consumers; - public DevChannelInfo(String name, Component publisher, List consumers) { + public DevChannelInfo(String name, List publishers, List consumers) { this.name = name; - this.publisher = publisher; + this.publishers = publishers != null ? publishers : Collections.emptyList(); this.consumers = consumers != null ? consumers : Collections.emptyList(); } @@ -126,8 +133,8 @@ public String getName() { return name; } - public Component getPublisher() { - return publisher; + public List getPublishers() { + return publishers; } public List getConsumers() { @@ -136,17 +143,11 @@ public List getConsumers() { @Override public int compareTo(DevChannelInfo other) { - if (publisher != other.publisher) { - if (other.publisher == null) { - return -1; - } - if (publisher == null) { - return 1; - } - // publisher connectors first - if (publisher.type != other.publisher.type) { - return publisher.type == ComponentType.CONNECTOR ? -1 : 1; - } + // publisher connectors last + long publisherConnectors = publishers.stream().filter(Component::isConnector).count(); + long otherPublisherConnectors = other.publishers.stream().filter(Component::isConnector).count(); + if (publisherConnectors != otherPublisherConnectors) { + return Long.compare(otherPublisherConnectors, publisherConnectors); } // consumer connectors last long consumerConnectors = consumers.stream().filter(Component::isConnector).count(); @@ -154,10 +155,6 @@ public int compareTo(DevChannelInfo other) { if (consumerConnectors != otherConsumersConnectors) { return Long.compare(otherConsumersConnectors, consumerConnectors); } - if (publisher != other.publisher && publisher.type == ComponentType.CONNECTOR - && other.publisher.type != ComponentType.CONNECTOR) { - return 1; - } // alphabetically return name.compareTo(other.name); } diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java index 3e83d74354348d..57554fb7e17853 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java @@ -24,7 +24,7 @@ public JsonArray getInfo() { private JsonObject toJson(DevReactiveMessagingInfos.DevChannelInfo channel) { JsonObject json = new JsonObject(); json.put("name", channel.getName()); - json.put("publisher", toJson(channel.getPublisher())); + json.put("publishers", toJson(channel.getPublishers())); json.put("consumers", toJson(channel.getConsumers())); return json; } diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java index 14a743cdac5564..c96ccd7b1ffc7b 100644 --- a/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java @@ -43,9 +43,9 @@ public void testProcessor() throws Exception { consumerExists = typeAndDescriptionExist(consumers, "CHANNEL", "io.quarkus.test.devui.MyProcessor#channel"); } - JsonNode publisher = channel.get("publisher"); - if (publisher != null) { - publisherExists = typeAndDescriptionExist(publisher, "PROCESSOR", + JsonNode publishers = channel.get("publishers"); + if (publishers != null) { + publisherExists = typeAndDescriptionExist(publishers, "PROCESSOR", "io.quarkus.test.devui.MyProcessor#process()"); } } From 9673b48e61425ae7716a45897ec7905cb233b599 Mon Sep 17 00:00:00 2001 From: brunobat Date: Thu, 9 Nov 2023 10:01:43 +0000 Subject: [PATCH 21/68] Send host.name in all spans --- .../deployment/OpenTelemetryResourceTest.java | 2 ++ ...ConfiguredOpenTelemetrySdkBuilderCustomizer.java | 13 ++++++++++++- .../opentelemetry/runtime/tracing/TracerUtil.java | 7 ++++++- .../runtime/tracing/TracerUtilTest.java | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java index c1e2c9bfef298e..92c919c44fe800 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java @@ -4,6 +4,7 @@ import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.List; @@ -54,6 +55,7 @@ void resource() { assertEquals("authservice", server.getResource().getAttribute(AttributeKey.stringKey("service.name"))); assertEquals(config.getRawValue("quarkus.uuid"), server.getResource().getAttribute(AttributeKey.stringKey("service.instance.id"))); + assertNotNull(server.getResource().getAttribute(AttributeKey.stringKey("host.name"))); } @Path("/hello") 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 cc9666624cd97f..de7e1ee998d961 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 @@ -3,6 +3,8 @@ import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; @@ -71,12 +73,21 @@ public Resource apply(Resource existingResource, ConfigProperties configProperti .filter(sn -> !sn.equals(appConfig.name.orElse("unset"))) .orElse(null); + // must be resolved at startup, once. + String hostname = null; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostname = "unknown"; + } + // Merge resource instances with env attributes Resource resource = resources.stream() .reduce(Resource.empty(), Resource::merge) .merge(TracerUtil.mapResourceAttributes( oTelRuntimeConfig.resourceAttributes().orElse(emptyList()), - serviceName)); // from properties + serviceName, // from properties + hostname)); return consolidatedResource.merge(resource); } else { return Resource.builder().build(); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java index 84ebd239996fa2..9c8e915904a04d 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java @@ -1,5 +1,6 @@ package io.quarkus.opentelemetry.runtime.tracing; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.HOST_NAME; import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; import java.util.List; @@ -14,7 +15,7 @@ public class TracerUtil { private TracerUtil() { } - public static Resource mapResourceAttributes(List resourceAttributes, String serviceName) { + public static Resource mapResourceAttributes(List resourceAttributes, String serviceName, String hostname) { final AttributesBuilder attributesBuilder = Attributes.builder(); if (!resourceAttributes.isEmpty()) { @@ -27,6 +28,10 @@ public static Resource mapResourceAttributes(List resourceAttributes, St attributesBuilder.put(SERVICE_NAME.getKey(), serviceName); } + if (hostname != null) { + attributesBuilder.put(HOST_NAME, hostname); + } + return Resource.create(attributesBuilder.build()); } } diff --git a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java index c3b27767970ef7..99c005ce5ed91a 100644 --- a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java +++ b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java @@ -19,7 +19,7 @@ public void testMapResourceAttributes() { "service.namespace=mynamespace", "service.version=1.0", "deployment.environment=production"); - Resource resource = TracerUtil.mapResourceAttributes(resourceAttributes, null); + Resource resource = TracerUtil.mapResourceAttributes(resourceAttributes, null, null); Attributes attributes = resource.getAttributes(); Assertions.assertThat(attributes.size()).isEqualTo(4); Assertions.assertThat(attributes.get(ResourceAttributes.SERVICE_NAME)).isEqualTo("myservice"); From 05b4a4e6cb7274e074dd050302e00389f1433a2f Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 9 Nov 2023 11:18:05 +0100 Subject: [PATCH 22/68] Build cache - Make sure there is a new line before EOF --- .github/workflows/develocity-publish-build-scans.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml index b269a77a6c5603..a285e66a8445be 100644 --- a/.github/workflows/develocity-publish-build-scans.yml +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -22,6 +22,7 @@ jobs: run: | echo "preapproved-developpers<> $GITHUB_OUTPUT cat .github/develocity-preapproved-developers.json >> $GITHUB_OUTPUT + echo >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Publish Maven Build Scans uses: gradle/github-actions/maven-build-scan/publish@v1-beta @@ -31,6 +32,7 @@ jobs: develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} skip-comment: true - name: Push to summary + if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} run: | echo -n "Pull request: " >> ${GITHUB_STEP_SUMMARY} cat pr-number.out >> ${GITHUB_STEP_SUMMARY} From cff11749369b148b9a3029fd52d194e0ad905e30 Mon Sep 17 00:00:00 2001 From: Torben Meyer Date: Thu, 9 Nov 2023 13:08:33 +0100 Subject: [PATCH 23/68] Fix discarded ObjectMapper configuration --- .../deployment/test/CustomSerializerTest.java | 120 ++++++++++++++++++ .../jackson/JacksonMessageBodyWriterUtil.java | 1 + 2 files changed, 121 insertions(+) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializerTest.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializerTest.java new file mode 100644 index 00000000000000..d8ba0287c1dd42 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializerTest.java @@ -0,0 +1,120 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.Comparator; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Provider; + +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.reactive.RestResponse.StatusCode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import io.quarkus.arc.Unremovable; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.internal.mapping.Jackson2Mapper; +import io.restassured.response.Response; + +public class CustomSerializerTest { + private static final OffsetDateTime FIXED_TIME = OffsetDateTime.now(); + private static final Jackson2Mapper MAPPER = new Jackson2Mapper((type, charset) -> { + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + return mapper; + }); + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().withEmptyApplication(); + + @Test + void shouldUseModulesInCustomSerializer() { + final Response response = RestAssured.given().get("custom-serializer"); + Assertions.assertThat(response.statusCode()).isEqualTo(StatusCode.OK); + + final CustomData actual = response.as(CustomData.class, MAPPER); + final CustomData expected = new CustomData("test-data", FIXED_TIME); + Assertions.assertThat(actual) + .usingComparatorForType(Comparator.comparing(OffsetDateTime::toInstant), OffsetDateTime.class) + .usingRecursiveComparison() + .isEqualTo(expected); + } + + @Path("custom-serializer") + @Produces(MediaType.APPLICATION_JSON) + static class CustomJacksonEndpoint { + + @GET + public CustomData getCustom() { + return new CustomData("test-data", FIXED_TIME); + } + } + + static class CustomData { + private final String name; + private final OffsetDateTime time; + + @JsonCreator + CustomData(@JsonProperty("name") final String name, @JsonProperty("time") final OffsetDateTime time) { + this.name = name; + this.time = time; + } + + public String getName() { + return this.name; + } + + public OffsetDateTime getTime() { + return this.time; + } + } + + static class CustomDataSerializer extends StdSerializer { + CustomDataSerializer() { + super(CustomData.class); + } + + @Override + public void serialize(final CustomData customData, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("name", customData.getName()); + if (customData.getTime() != null) { + jsonGenerator.writeObjectField("time", customData.getTime()); + } + jsonGenerator.writeEndObject(); + } + } + + @Provider + @Unremovable + public static class CustomObjectMapperContextResolver implements ContextResolver { + + @Override + public ObjectMapper getContext(final Class type) { + final ObjectMapper objectMapper = new ObjectMapper(); + final SimpleModule simpleModule = new SimpleModule("custom-data"); + simpleModule.addSerializer(new CustomDataSerializer()); + objectMapper.registerModule(simpleModule); + objectMapper.registerModule(new JavaTimeModule()); + return objectMapper; + } + } + +} diff --git a/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonMessageBodyWriterUtil.java b/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonMessageBodyWriterUtil.java index d5e9f631a2b17a..3b004d2236ae93 100644 --- a/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonMessageBodyWriterUtil.java +++ b/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonMessageBodyWriterUtil.java @@ -28,6 +28,7 @@ public static ObjectWriter createDefaultWriter(ObjectMapper mapper) { if (JacksonMessageBodyWriterUtil.needsNewFactory(jsonFactory)) { jsonFactory = jsonFactory.copy(); JacksonMessageBodyWriterUtil.setNecessaryJsonFactoryConfig(jsonFactory); + jsonFactory.setCodec(mapper); return mapper.writer().with(jsonFactory); } else { return mapper.writer(); From f8c6f3edc47294952454e9761fe2093af12679e0 Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Thu, 9 Nov 2023 16:38:07 +0100 Subject: [PATCH 24/68] Updates to Infinispan 14.0.20.Final --- 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 12ba92c88e127e..fc4b7b4d14625a 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -140,7 +140,7 @@ 2.2 5.10.0 1.5.0 - 14.0.19.Final + 14.0.20.Final 4.6.5.Final 3.1.5 4.1.100.Final From da2b6ca48e65d9ceadc606a52042ea978bd33334 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 9 Nov 2023 18:27:14 +0100 Subject: [PATCH 25/68] Generate a file with relations between guides Also adjust a few topics in guides and removed the quarkus-core extension as it linked guides which had nothing to do together. --- docs/src/main/asciidoc/amqp-dev-services.adoc | 2 +- .../apicurio-registry-dev-services.adoc | 2 +- docs/src/main/asciidoc/appcds.adoc | 1 - docs/src/main/asciidoc/build-analytics.adoc | 1 - .../main/asciidoc/building-native-image.adoc | 1 - docs/src/main/asciidoc/capabilities.adoc | 1 - .../asciidoc/class-loading-reference.adoc | 1 - .../main/asciidoc/command-mode-reference.adoc | 1 - .../conditional-extension-dependencies.adoc | 1 - .../asciidoc/config-extending-support.adoc | 1 - docs/src/main/asciidoc/config-mappings.adoc | 1 - docs/src/main/asciidoc/config-reference.adoc | 1 - docs/src/main/asciidoc/config.adoc | 1 - .../main/asciidoc/context-propagation.adoc | 1 - .../src/main/asciidoc/continuous-testing.adoc | 3 +- .../main/asciidoc/databases-dev-services.adoc | 2 +- .../main/asciidoc/dev-mode-differences.adoc | 3 +- docs/src/main/asciidoc/dev-services.adoc | 2 +- docs/src/main/asciidoc/dev-ui.adoc | 3 +- .../asciidoc/elasticsearch-dev-services.adoc | 2 +- .../main/asciidoc/extension-codestart.adoc | 1 - .../src/main/asciidoc/extension-metadata.adoc | 1 - .../getting-started-dev-services.adoc | 2 +- .../asciidoc/infinispan-dev-services.adoc | 2 +- .../src/main/asciidoc/kafka-dev-services.adoc | 2 +- docs/src/main/asciidoc/kafka-dev-ui.adoc | 2 +- .../asciidoc/kubernetes-dev-services.adoc | 2 +- docs/src/main/asciidoc/lifecycle.adoc | 2 +- docs/src/main/asciidoc/logging.adoc | 1 - .../main/asciidoc/pulsar-dev-services.adoc | 2 +- .../main/asciidoc/rabbitmq-dev-services.adoc | 2 +- .../src/main/asciidoc/rabbitmq-reference.adoc | 2 +- docs/src/main/asciidoc/rabbitmq.adoc | 2 +- .../src/main/asciidoc/redis-dev-services.adoc | 2 +- .../security-openid-connect-dev-services.adoc | 2 +- docs/src/main/asciidoc/virtual-threads.adoc | 1 - .../generation/YamlMetadataGenerator.java | 116 ++++++++++++++++++ docs/sync-web-site.sh | 9 ++ 38 files changed, 145 insertions(+), 39 deletions(-) diff --git a/docs/src/main/asciidoc/amqp-dev-services.adoc b/docs/src/main/asciidoc/amqp-dev-services.adoc index 2c1bb488b362e1..f68fb219e3abce 100644 --- a/docs/src/main/asciidoc/amqp-dev-services.adoc +++ b/docs/src/main/asciidoc/amqp-dev-services.adoc @@ -8,7 +8,7 @@ include::_attributes.adoc[] :categories: messaging :summary: Start AMQP automatically in dev and test modes. :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-amqp -:topics: messaging,amqp,devservices,tooling,testing,devmode +:topics: messaging,amqp,dev-services,testing,dev-mode Dev Services for AMQP automatically starts an AMQP 1.0 broker in dev mode and when running tests. So, you don't have to start a broker manually. diff --git a/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc b/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc index 0eb728d1d844a5..fc4f80b948176f 100644 --- a/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc +++ b/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Start Apicurio Registry automatically in dev and test modes. -:topics: messaging,kafka,apicurio,registry,devservices,tooling,testing,devmode +:topics: messaging,kafka,apicurio,registry,dev-services,dev-mode,testing :extensions: io.quarkus:quarkus-apicurio-registry-avro,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If an extension for schema registry, such as `quarkus-apicurio-registry-avro` or `quarkus-confluent-registry-avro`, is present, Dev Services for Apicurio Registry automatically starts an Apicurio Registry instance in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/appcds.adoc b/docs/src/main/asciidoc/appcds.adoc index 543d7da0c8a7d4..59a82eaa0e8c5c 100644 --- a/docs/src/main/asciidoc/appcds.adoc +++ b/docs/src/main/asciidoc/appcds.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core, cloud :summary: This reference guide explains how to enable AppCDS with Quarkus. :topics: appcds,serverless -:extensions: io.quarkus:quarkus-core This reference guide explains how to enable Application Class Data Sharing in your Quarkus applications. diff --git a/docs/src/main/asciidoc/build-analytics.adoc b/docs/src/main/asciidoc/build-analytics.adoc index 820c2d34f868b7..ff94a46c57488e 100644 --- a/docs/src/main/asciidoc/build-analytics.adoc +++ b/docs/src/main/asciidoc/build-analytics.adoc @@ -6,7 +6,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Build analytics :categories: analytics :summary: This guide presents what build analytics is and how to configure it. -:extensions: io.quarkus:quarkus-core The Quarkus team has limited knowledge, from Maven download numbers, of the remarkable growth of Quarkus and the number of users reporting issues/concerns. Still, we need more insight into the platforms, operating system, Java combinations, and build tools our users employ. The build analytics tool aims to provide us with this information. diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index 4353b5fbff90ea..96e4e1640cdb01 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: getting-started, native :summary: Build native executables with GraalVM or Mandrel. :topics: native,graalvm,mandrel -:extensions: io.quarkus:quarkus-core This guide covers: diff --git a/docs/src/main/asciidoc/capabilities.adoc b/docs/src/main/asciidoc/capabilities.adoc index 6ef79dbf6a48fe..149fe8e36bfe77 100644 --- a/docs/src/main/asciidoc/capabilities.adoc +++ b/docs/src/main/asciidoc/capabilities.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: How capabilities are implemented and used in Quarkus. :topics: extensions -:extensions: io.quarkus:quarkus-core Quarkus extensions may provide certain capabilities and require certain capabilities to be provided by other extensions in an application to function properly. diff --git a/docs/src/main/asciidoc/class-loading-reference.adoc b/docs/src/main/asciidoc/class-loading-reference.adoc index 65cd0c98e58289..6b740d078c24c0 100644 --- a/docs/src/main/asciidoc/class-loading-reference.adoc +++ b/docs/src/main/asciidoc/class-loading-reference.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: architecture :summary: Learn more about Quarkus class loading infrastructure. :topics: internals,extensions -:extensions: io.quarkus:quarkus-core This document explains the Quarkus class loading architecture. It is intended for extension authors and advanced users who want to understand exactly how Quarkus works. diff --git a/docs/src/main/asciidoc/command-mode-reference.adoc b/docs/src/main/asciidoc/command-mode-reference.adoc index 7fc9b2a7ecdd8e..c4aeb11dbdaa9c 100644 --- a/docs/src/main/asciidoc/command-mode-reference.adoc +++ b/docs/src/main/asciidoc/command-mode-reference.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core, command-line :summary: This reference guide explains how to develop command line applications with Quarkus. :topics: command-line,cli -:extensions: io.quarkus:quarkus-core This reference covers how to write applications that run and then exit. diff --git a/docs/src/main/asciidoc/conditional-extension-dependencies.adoc b/docs/src/main/asciidoc/conditional-extension-dependencies.adoc index 1aec001d92ad96..576f4424f484e5 100644 --- a/docs/src/main/asciidoc/conditional-extension-dependencies.adoc +++ b/docs/src/main/asciidoc/conditional-extension-dependencies.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: Trigger the inclusion on additional extensions based on certain conditions. :topics: extensions -:extensions: io.quarkus:quarkus-core Quarkus extension dependencies are usually configured in the same way as any other project dependencies in the project's build file, e.g. the Maven `pom.xml` or the Gradle build scripts. However, there are dependency types that aren't yet supported out-of-the-box by Maven and Gradle. What we refer here to as "conditional dependencies" is one example. diff --git a/docs/src/main/asciidoc/config-extending-support.adoc b/docs/src/main/asciidoc/config-extending-support.adoc index aca5e13344863e..0a4b215fabbfbb 100644 --- a/docs/src/main/asciidoc/config-extending-support.adoc +++ b/docs/src/main/asciidoc/config-extending-support.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core [[custom-config-source]] == Custom `ConfigSource` diff --git a/docs/src/main/asciidoc/config-mappings.adoc b/docs/src/main/asciidoc/config-mappings.adoc index 89d8265828c006..61cb45b4839234 100644 --- a/docs/src/main/asciidoc/config-mappings.adoc +++ b/docs/src/main/asciidoc/config-mappings.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core With config mappings it is possible to group multiple configuration properties in a single interface that share the same prefix. diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 3703e48677817a..e3c22718953a04 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core IMPORTANT: The content of this guide has been revised and split into additional topics. Please check the <> section. diff --git a/docs/src/main/asciidoc/config.adoc b/docs/src/main/asciidoc/config.adoc index 4ef6b3cd9b2624..1ae0e1a14e7257 100644 --- a/docs/src/main/asciidoc/config.adoc +++ b/docs/src/main/asciidoc/config.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core :summary: Hardcoded values in your code is a no go (even if we all did it at some point ;-)). In this guide, we learn how to configure your application. :topics: configuration -:extensions: io.quarkus:quarkus-core IMPORTANT: The content of this guide and been revised and split into additional topics. Please check the <> section. diff --git a/docs/src/main/asciidoc/context-propagation.adoc b/docs/src/main/asciidoc/context-propagation.adoc index deffd04652f2f3..89955b1eddc97f 100644 --- a/docs/src/main/asciidoc/context-propagation.adoc +++ b/docs/src/main/asciidoc/context-propagation.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core :summary: Learn more about how you can pass contextual information with SmallRye Context Propagation. :topics: context-propagation -:extensions: io.quarkus:quarkus-core Traditional blocking code uses link:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ThreadLocal.html[`ThreadLocal`] variables to store contextual objects in order to avoid diff --git a/docs/src/main/asciidoc/continuous-testing.adoc b/docs/src/main/asciidoc/continuous-testing.adoc index 6be909bc953875..ef74b9c900785a 100644 --- a/docs/src/main/asciidoc/continuous-testing.adoc +++ b/docs/src/main/asciidoc/continuous-testing.adoc @@ -10,8 +10,7 @@ include::_attributes.adoc[] :numbered: :sectnums: :sectnumlevels: 4 -:topics: testing,dev-ui,tooling,devmode -:extensions: io.quarkus:quarkus-core +:topics: testing,dev-ui,tooling,dev-mode Learn how to use continuous testing in your Quarkus Application. diff --git a/docs/src/main/asciidoc/databases-dev-services.adoc b/docs/src/main/asciidoc/databases-dev-services.adoc index f30efd79d9f482..2590c8751cac8c 100644 --- a/docs/src/main/asciidoc/databases-dev-services.adoc +++ b/docs/src/main/asciidoc/databases-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for Databases :categories: data, tooling include::_attributes.adoc[] -:topics: devservices,data,database,datasource,tooling,testing,devmode +:topics: dev-services,data,database,datasource,dev-mode,testing :extensions: io.quarkus:quarkus-agroal,io.quarkus:quarkus-reactive-mysql-client,io.quarkus:quarkus-reactive-oracle-client,io.quarkus:quarkus-reactive-pg-client,io.quarkus:quarkus-jdbc-db2,io.quarkus:quarkus-jdbc-derby,io.quarkus:quarkus-jdbc-h2,io.quarkus:quarkus-jdbc-mariadb,io.quarkus:quarkus-jdbc-mssql,io.quarkus:quarkus-jdbc-mysql,io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-postgresql When testing or running in dev mode Quarkus can provide you with a zero-config database out of the box, a feature we refer to as Dev Services. diff --git a/docs/src/main/asciidoc/dev-mode-differences.adoc b/docs/src/main/asciidoc/dev-mode-differences.adoc index ed3312ac24a329..b49ee27d3ca6bc 100644 --- a/docs/src/main/asciidoc/dev-mode-differences.adoc +++ b/docs/src/main/asciidoc/dev-mode-differences.adoc @@ -7,8 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: architecture :summary: How dev mode differs from a production application -:topics: internals,devmode -:extensions: io.quarkus:quarkus-core +:topics: internals,dev-mode This document explains how the dev mode in Quarkus differs from a production application. diff --git a/docs/src/main/asciidoc/dev-services.adoc b/docs/src/main/asciidoc/dev-services.adoc index 6e8517de9f7c42..39c49c6c55d391 100644 --- a/docs/src/main/asciidoc/dev-services.adoc +++ b/docs/src/main/asciidoc/dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: core :summary: A list of all extensions that support Dev Services and their configuration options. -:topics: devservices,tooling,testing,devmode +:topics: dev-services,dev-mode,testing Quarkus supports the automatic provisioning of unconfigured services in development and test mode. We refer to this capability as Dev Services. From a developer's perspective this means that if you include an extension and don't configure it then diff --git a/docs/src/main/asciidoc/dev-ui.adoc b/docs/src/main/asciidoc/dev-ui.adoc index 83fc9c33b9871f..38bcbb28de722b 100644 --- a/docs/src/main/asciidoc/dev-ui.adoc +++ b/docs/src/main/asciidoc/dev-ui.adoc @@ -7,8 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: writing-extensions :summary: Learn how to get your extension to contribute features to the Dev UI (v2). -:topics: dev-ui,tooling,testing -:extensions: io.quarkus:quarkus-core +:topics: dev-ui,testing [NOTE] .Dev UI v2 diff --git a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc index 2ede49bdb9c1fd..5d66575584a33a 100644 --- a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc +++ b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Elasticsearch automatically in dev and test modes -:topics: data,search,elasticsearch,nosql,devservices,tooling,testing,devmode +:topics: data,search,elasticsearch,nosql,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-elasticsearch-java-client,io.quarkus:quarkus-elasticsearch-rest-client,io.quarkus:quarkus-hibernate-search-orm-elasticsearch If any Elasticsearch-related extension is present (e.g. `quarkus-elasticsearch-rest-client` or `quarkus-hibernate-search-orm-elasticsearch`), diff --git a/docs/src/main/asciidoc/extension-codestart.adoc b/docs/src/main/asciidoc/extension-codestart.adoc index 78d63ff702a734..5f36ce736aa24c 100644 --- a/docs/src/main/asciidoc/extension-codestart.adoc +++ b/docs/src/main/asciidoc/extension-codestart.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: Provide users with initial code for extensions when generating Quarkus applications on code.quarkus.io and all the Quarkus tooling. This guide explains how to create and configure a Codestart for an extension. :topics: extensions,codestarts -:extensions: io.quarkus:quarkus-core This guide explains how to create and configure a Quarkus Codestart for an extension. diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc index 9b6f7ca09cf6a3..0a0f6f88e01a15 100644 --- a/docs/src/main/asciidoc/extension-metadata.adoc +++ b/docs/src/main/asciidoc/extension-metadata.adoc @@ -7,7 +7,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: writing-extensions :topics: extensions,codestarts -:extensions: io.quarkus:quarkus-core Quarkus extensions are distributed as Maven JAR artifacts that application and other libraries may depend on. When a Quarkus application project is built, tested or edited using the Quarkus dev tools, Quarkus extension JAR artifacts will be identified on the application classpath by the presence of the Quarkus extension metadata files in them. This document describes the purpose of each Quarkus extension metadata file and its content. diff --git a/docs/src/main/asciidoc/getting-started-dev-services.adoc b/docs/src/main/asciidoc/getting-started-dev-services.adoc index 8869b3fd521686..14f62ced451bbc 100644 --- a/docs/src/main/asciidoc/getting-started-dev-services.adoc +++ b/docs/src/main/asciidoc/getting-started-dev-services.adoc @@ -9,7 +9,7 @@ include::_attributes.adoc[] :diataxis-type: tutorial :categories: getting-started, data, core :summary: Discover some of the features that make developing with Quarkus a joyful experience. -:topics: getting-started,devservices +:topics: getting-started,dev-services This tutorial shows you how to create an application which writes to and reads from a database. You will use Dev Services, so you will not actually download, configure, or even start the database yourself. diff --git a/docs/src/main/asciidoc/infinispan-dev-services.adoc b/docs/src/main/asciidoc/infinispan-dev-services.adoc index c2dfc3140a3ad6..436ab6b5a198ca 100644 --- a/docs/src/main/asciidoc/infinispan-dev-services.adoc +++ b/docs/src/main/asciidoc/infinispan-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Infinispan automatically in dev and test modes. -:topics: devservices,data,infinispan,tooling,testing,devmode +:topics: dev-services,data,infinispan,testing,dev-mode :extensions: io.quarkus:quarkus-infinispan-client Quarkus supports a feature called Dev Services that allows you to create various datasources without any config. diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index b6d32ba55e9972..a94da965a3a21b 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Start Apache Kafka automatically in dev and test modes. -:topics: messaging,kafka,devservices,tooling,testing,devmode +:topics: messaging,kafka,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-kafka-client,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If any Kafka-related extension is present (e.g. `quarkus-smallrye-reactive-messaging-kafka`), Dev Services for Kafka automatically starts a Kafka broker in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/kafka-dev-ui.adoc b/docs/src/main/asciidoc/kafka-dev-ui.adoc index 2625df6a7e224b..90cf6335f6961e 100644 --- a/docs/src/main/asciidoc/kafka-dev-ui.adoc +++ b/docs/src/main/asciidoc/kafka-dev-ui.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Dev UI extension for Apache Kafka for development purposes. -:topics: messaging,kafka,dev-ui,devmode +:topics: messaging,kafka,dev-ui,dev-mode :extensions: io.quarkus:quarkus-kafka-client,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If any Kafka-related extension is present (e.g. `quarkus-smallrye-reactive-messaging-kafka`), diff --git a/docs/src/main/asciidoc/kubernetes-dev-services.adoc b/docs/src/main/asciidoc/kubernetes-dev-services.adoc index bd2e9bfa0e48f3..85f86d1ace39de 100644 --- a/docs/src/main/asciidoc/kubernetes-dev-services.adoc +++ b/docs/src/main/asciidoc/kubernetes-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: cloud :summary: Start a Kubernetes API server automatically in dev and test modes. -:topics: devservices,kubernetes,tooling,testing,devmode +:topics: dev-services,kubernetes,testing,dev-mode :extensions: io.quarkus:quarkus-kubernetes-client Dev Services for Kubernetes automatically starts a Kubernetes API server in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/lifecycle.adoc b/docs/src/main/asciidoc/lifecycle.adoc index 539b59006bb73e..911f10ff02f8b5 100644 --- a/docs/src/main/asciidoc/lifecycle.adoc +++ b/docs/src/main/asciidoc/lifecycle.adoc @@ -9,7 +9,7 @@ include::_attributes.adoc[] :keywords: lifecycle event :summary: You often need to execute custom actions when the application starts and clean up everything when the application stops. This guide explains how to be notified when an application stops or starts. :topics: lifecycle,observers -:extensions: io.quarkus:quarkus-core,io.quarkus:quarkus-arc +:extensions: io.quarkus:quarkus-arc You often need to execute custom actions when the application starts and clean up everything when the application stops. This guide explains how to: diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index c22add0efb800a..a9329d517f4b10 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -9,7 +9,6 @@ include::_attributes.adoc[] :categories: core,getting-started,observability :diataxis-type: reference :topics: logging,observability -:extensions: io.quarkus:quarkus-core Read about the use of logging API in Quarkus, configuring logging output, and using logging adapters to unify the output from other logging APIs. diff --git a/docs/src/main/asciidoc/pulsar-dev-services.adoc b/docs/src/main/asciidoc/pulsar-dev-services.adoc index 302f470f37749d..f1f8743bceadb4 100644 --- a/docs/src/main/asciidoc/pulsar-dev-services.adoc +++ b/docs/src/main/asciidoc/pulsar-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for Pulsar include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,pulsar,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,pulsar,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-pulsar With Quarkus Smallrye Reactive Messaging Pulsar extension (`quarkus-smallrye-reactive-messaging-pulsar`) diff --git a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc index c99349f7dd1e55..cf416e7ba075c7 100644 --- a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc +++ b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for RabbitMQ include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq Dev Services for RabbitMQ automatically starts a RabbitMQ broker in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/rabbitmq-reference.adoc b/docs/src/main/asciidoc/rabbitmq-reference.adoc index 586e338c046fc6..bd421a5b2b202a 100644 --- a/docs/src/main/asciidoc/rabbitmq-reference.adoc +++ b/docs/src/main/asciidoc/rabbitmq-reference.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :extension-status: preview :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq This guide is the companion from the xref:rabbitmq.adoc[Getting Started with RabbitMQ]. diff --git a/docs/src/main/asciidoc/rabbitmq.adoc b/docs/src/main/asciidoc/rabbitmq.adoc index 962edb7171c988..255969990afcf7 100644 --- a/docs/src/main/asciidoc/rabbitmq.adoc +++ b/docs/src/main/asciidoc/rabbitmq.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc :extension-status: preview include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq This guide demonstrates how your Quarkus application can utilize SmallRye Reactive Messaging to interact with RabbitMQ. diff --git a/docs/src/main/asciidoc/redis-dev-services.adoc b/docs/src/main/asciidoc/redis-dev-services.adoc index 0572c08e4343ae..737f4e0e0d8bbe 100644 --- a/docs/src/main/asciidoc/redis-dev-services.adoc +++ b/docs/src/main/asciidoc/redis-dev-services.adoc @@ -8,7 +8,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Redis automatically in dev and test modes. -:topics: data,redis,nosql,devservices,tooling,testing,devmode +:topics: data,redis,nosql,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-redis-client Quarkus supports a feature called Dev Services that allows you to create various datasources without any config. diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index c405cabb19516d..3e4f7fc20672ef 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -8,7 +8,7 @@ include::_attributes.adoc[] :categories: security :keywords: sso oidc security keycloak :summary: Start Keycloak or other providers automatically in dev and test modes. -:topics: security,oidc,keycloak,devservices,tooling,testing,devmode +:topics: security,oidc,keycloak,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-oidc This guide covers the Dev Services and UI for OpenID Connect (OIDC) Keycloak provider and explains how to support Dev Services and UI for other OpenID Connect providers. diff --git a/docs/src/main/asciidoc/virtual-threads.adoc b/docs/src/main/asciidoc/virtual-threads.adoc index 732931232ddb66..eba2cde1575509 100644 --- a/docs/src/main/asciidoc/virtual-threads.adoc +++ b/docs/src/main/asciidoc/virtual-threads.adoc @@ -15,7 +15,6 @@ include::_attributes.adoc[] :thread: https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/Thread.html :pgsql-driver: https://javadoc.io/doc/org.postgresql/postgresql/latest/index.html :topics: virtual-threads -:extensions: io.quarkus:quarkus-core This guide explains how to benefit from Java 21+ virtual threads in Quarkus application. diff --git a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java index 162c4df7484811..79048b6c42731b 100644 --- a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java +++ b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java @@ -7,11 +7,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.TreeMap; @@ -27,6 +29,7 @@ import org.asciidoctor.ast.Document; import org.asciidoctor.ast.StructuralNode; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.exc.StreamWriteException; @@ -55,6 +58,8 @@ public class YamlMetadataGenerator { final static String INCL_ATTRIBUTES = "include::_attributes.adoc[]\n"; final static String YAML_FRONTMATTER = "---\n"; + private static final String COMPATIBILITY_TOPIC = "compatibility"; + public static void main(String[] args) throws Exception { System.out.println("[INFO] Creating YAML metadata generator: " + List.of(args)); YamlMetadataGenerator generator = new YamlMetadataGenerator() @@ -120,6 +125,8 @@ public void writeYamlFiles() throws StreamWriteException, DatabindException, IOE om.writeValue(targetDir.resolve("indexByType.yaml").toFile(), index); om.writeValue(targetDir.resolve("indexByFile.yaml").toFile(), metadata); + om.writeValue(targetDir.resolve("relations.yaml").toFile(), index.relationsByFile(metadata)); + om.writeValue(targetDir.resolve("errorsByType.yaml").toFile(), messages); om.writeValue(targetDir.resolve("errorsByFile.yaml").toFile(), messages.allByFile()); } @@ -455,6 +462,50 @@ public Map metadataByFile() { .collect(Collectors.toMap(v -> v.filename, v -> v, (o1, o2) -> o1, TreeMap::new)); } + public Map relationsByFile(Map metadataByFile) { + Map relationsByFile = new TreeMap<>(); + + for (Entry currentMetadataEntry : metadataByFile.entrySet()) { + DocRelations docRelations = new DocRelations(); + + for (Entry candidateMetadataEntry : metadataByFile.entrySet()) { + if (candidateMetadataEntry.getKey().equals(currentMetadataEntry.getKey())) { + continue; + } + + DocMetadata candidateMetadata = candidateMetadataEntry.getValue(); + int extensionMatches = 0; + for (String extension : currentMetadataEntry.getValue().getExtensions()) { + if (candidateMetadata.getExtensions().contains(extension)) { + extensionMatches++; + } + } + if (extensionMatches > 0) { + docRelations.sameExtensions.add( + new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), extensionMatches)); + } + + int topicMatches = 0; + for (String topic : currentMetadataEntry.getValue().getTopics()) { + if (candidateMetadata.getTopics().contains(topic)) { + topicMatches++; + } + } + if (topicMatches > 0 && (!candidateMetadata.getTopics().contains(COMPATIBILITY_TOPIC) + || currentMetadataEntry.getValue().getTopics().contains(COMPATIBILITY_TOPIC))) { + docRelations.sameTopics + .add(new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), topicMatches)); + } + } + + if (!docRelations.isEmpty()) { + relationsByFile.put(currentMetadataEntry.getKey(), docRelations); + } + } + + return relationsByFile; + } + // convenience public Map messagesByFile() { return messages.allByFile(); @@ -595,6 +646,71 @@ public int compareTo(DocMetadata that) { } } + @JsonInclude(value = Include.NON_EMPTY) + public static class DocRelations { + + Set sameTopics = new TreeSet<>(DocRelationComparator.INSTANCE); + + Set sameExtensions = new TreeSet<>(DocRelationComparator.INSTANCE); + + public Set getSameTopics() { + return sameTopics; + } + + public Set getSameExtensions() { + return sameExtensions; + } + + @JsonIgnore + public boolean isEmpty() { + return sameTopics.isEmpty() && sameExtensions.isEmpty(); + } + } + + @JsonInclude(value = Include.NON_EMPTY) + public static class DocRelation { + + String title; + + String url; + + int matches; + + DocRelation(String title, String url, int matches) { + this.title = title; + this.url = url; + this.matches = matches; + } + + public String getTitle() { + return title; + } + + public String getUrl() { + return url; + } + + public int getMatches() { + return matches; + } + } + + public static class DocRelationComparator implements Comparator { + + static final DocRelationComparator INSTANCE = new DocRelationComparator(); + + @Override + public int compare(DocRelation o1, DocRelation o2) { + int compareMatches = o2.matches - o1.matches; + + if (compareMatches != 0) { + return compareMatches; + } + + return o1.title.compareToIgnoreCase(o2.title); + } + } + @JsonInclude(value = Include.NON_EMPTY) public static class FileMessages { Collection errors; diff --git a/docs/sync-web-site.sh b/docs/sync-web-site.sh index 3e323cd748ec01..1041831b124778 100755 --- a/docs/sync-web-site.sh +++ b/docs/sync-web-site.sh @@ -120,6 +120,15 @@ if [ -f target/indexByType.yaml ]; then echo fi +if [ -f target/relations.yaml ]; then + echo + echo "Copying target/relations.yaml to $TARGET_INDEX/relations.yaml" + mkdir -p $TARGET_INDEX + echo "# Generated file. Do not edit" > $TARGET_INDEX/relations.yaml + cat target/relations.yaml >> $TARGET_INDEX/relations.yaml + echo +fi + echo "Sync done!" echo "==========" From 43b7e08590825cf9593652bf087ec16f0b63e6c5 Mon Sep 17 00:00:00 2001 From: Eric Deandrea Date: Tue, 7 Nov 2023 15:41:05 -0500 Subject: [PATCH 26/68] Options for generating gRPC descriptor set --- .../asciidoc/grpc-generation-reference.adoc | 15 +++ .../quarkus/grpc/deployment/GrpcCodeGen.java | 28 +++++ .../build.gradle | 33 ++++++ .../gradle.properties | 2 + .../settings.gradle | 16 +++ .../acme/quarkus/sample/HelloResource.java | 21 ++++ .../src/main/proto/hello.proto | 23 ++++ .../src/main/resources/application.properties | 3 + .../build.gradle | 33 ++++++ .../gradle.properties | 2 + .../settings.gradle | 16 +++ .../acme/quarkus/sample/HelloResource.java | 21 ++++ .../src/main/proto/hello.proto | 23 ++++ .../src/main/resources/application.properties | 2 + .../grpc-descriptor-set/build.gradle | 33 ++++++ .../grpc-descriptor-set/gradle.properties | 2 + .../grpc-descriptor-set/settings.gradle | 16 +++ .../acme/quarkus/sample/HelloResource.java | 21 ++++ .../src/main/proto/hello.proto | 23 ++++ .../src/main/resources/application.properties | 1 + ...DescriptorSetAlternateOutputBuildTest.java | 27 +++++ ...criptorSetAlternateOutputDirBuildTest.java | 24 ++++ .../gradle/GrpcDescriptorSetBuildTest.java | 27 +++++ .../pom.xml | 28 +++++ .../examples/hello/HelloWorldEndpoint.java | 41 +++++++ .../examples/hello/HelloWorldService.java | 23 ++++ .../src/main/proto/helloworld.proto | 21 ++++ .../src/main/resources/application.properties | 12 ++ .../hello/DescriptorSetExistsTest.java | 21 ++++ .../pom.xml | 28 +++++ .../examples/hello/HelloWorldEndpoint.java | 41 +++++++ .../examples/hello/HelloWorldService.java | 23 ++++ .../src/main/proto/helloworld.proto | 21 ++++ .../src/main/resources/application.properties | 11 ++ .../hello/DescriptorSetExistsTest.java | 22 ++++ .../grpc-descriptor-set/pom.xml | 28 +++++ .../examples/hello/HelloWorldEndpoint.java | 41 +++++++ .../examples/hello/HelloWorldService.java | 23 ++++ .../src/main/proto/helloworld.proto | 21 ++++ .../src/main/resources/application.properties | 10 ++ .../hello/DescriptorSetExistsTest.java | 22 ++++ .../grpc-descriptor-sets/pom.xml | 105 ++++++++++++++++++ integration-tests/pom.xml | 1 + 43 files changed, 955 insertions(+) create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/build.gradle create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/gradle.properties create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/settings.gradle create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/java/org/acme/quarkus/sample/HelloResource.java create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/proto/hello.proto create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/build.gradle create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/gradle.properties create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/settings.gradle create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/java/org/acme/quarkus/sample/HelloResource.java create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/proto/hello.proto create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/resources/application.properties create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set/gradle.properties create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set/settings.gradle create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/java/org/acme/quarkus/sample/HelloResource.java create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/proto/hello.proto create mode 100644 integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/resources/application.properties create mode 100644 integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputBuildTest.java create mode 100644 integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputDirBuildTest.java create mode 100644 integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetBuildTest.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/pom.xml create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/proto/helloworld.proto create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/pom.xml create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/proto/helloworld.proto create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/resources/application.properties create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set/pom.xml create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/proto/helloworld.proto create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/resources/application.properties create mode 100644 integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java create mode 100644 integration-tests/grpc-descriptor-sets/pom.xml diff --git a/docs/src/main/asciidoc/grpc-generation-reference.adoc b/docs/src/main/asciidoc/grpc-generation-reference.adoc index 6d871816c3c338..48c732bcd5123e 100644 --- a/docs/src/main/asciidoc/grpc-generation-reference.adoc +++ b/docs/src/main/asciidoc/grpc-generation-reference.adoc @@ -99,6 +99,21 @@ quarkus { } ---- +== Generating Descriptor Set +Protocol Buffers do not contain descriptions of their own types. Thus, given only a raw message without the corresponding .proto file defining its type, it is difficult to extract any useful data. However, the contents of a .proto file can itself be https://protobuf.dev/programming-guides/techniques/#self-description[represented using protocol buffers]. + +By default, Quarkus does not generate these descriptors. Quarkus does provide several configuration options for generating them. These would be added to your `application.properties` or `application.yml` file: + +* `quarkus.generate-code.grpc.descriptor-set.generate` +** Set to `true` to enable generation +* `quarkus.generate-code.grpc.descriptor-set.output-dir` +** Set this to a value relative to the project's build directory (i.e. `target` for Maven, `build` for Gradle) +** Maven default value: `target/generated-sources/grpc` +** Gradle default value: `$buildDir/classes/java/quarkus-generated-sources/grpc` +* `quarkus.generate-code.grpc.descriptor-set.name` +** Name of the descriptor set file to generate +** Default value: `descriptor_set.dsc` + == Configuring gRPC code generation for dependencies You may have dependencies that contain `\*.proto` files you want to compile to Java sources. diff --git a/extensions/grpc/codegen/src/main/java/io/quarkus/grpc/deployment/GrpcCodeGen.java b/extensions/grpc/codegen/src/main/java/io/quarkus/grpc/deployment/GrpcCodeGen.java index 32a0ed9d061726..6c5417239d0a39 100644 --- a/extensions/grpc/codegen/src/main/java/io/quarkus/grpc/deployment/GrpcCodeGen.java +++ b/extensions/grpc/codegen/src/main/java/io/quarkus/grpc/deployment/GrpcCodeGen.java @@ -1,5 +1,6 @@ package io.quarkus.grpc.deployment; +import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Arrays.asList; @@ -56,6 +57,9 @@ public class GrpcCodeGen implements CodeGenProvider { private static final String SCAN_FOR_IMPORTS = "quarkus.generate-code.grpc.scan-for-imports"; private static final String POST_PROCESS_SKIP = "quarkus.generate.code.grpc-post-processing.skip"; + private static final String GENERATE_DESCRIPTOR_SET = "quarkus.generate-code.grpc.descriptor-set.generate"; + private static final String DESCRIPTOR_SET_OUTPUT_DIR = "quarkus.generate-code.grpc.descriptor-set.output-dir"; + private static final String DESCRIPTOR_SET_FILENAME = "quarkus.generate-code.grpc.descriptor-set.name"; private Executables executables; private String input; @@ -149,6 +153,11 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { "--q-grpc_out=" + outDir, "--grpc_out=" + outDir, "--java_out=" + outDir)); + + if (shouldGenerateDescriptorSet(context.config())) { + command.add(String.format("--descriptor_set_out=%s", getDescriptorSetOutputFile(context))); + } + command.addAll(protoFiles); ProcessBuilder processBuilder = new ProcessBuilder(command); @@ -262,6 +271,25 @@ private boolean isGeneratingFromAppDependenciesEnabled(Config config) { .filter(value -> !"none".equals(value)).isPresent(); } + private boolean shouldGenerateDescriptorSet(Config config) { + return config.getOptionalValue(GENERATE_DESCRIPTOR_SET, Boolean.class).orElse(FALSE); + } + + private Path getDescriptorSetOutputFile(CodeGenContext context) throws IOException { + var dscOutputDir = context.config().getOptionalValue(DESCRIPTOR_SET_OUTPUT_DIR, String.class) + .map(context.workDir()::resolve) + .orElseGet(context::outDir); + + if (Files.notExists(dscOutputDir)) { + Files.createDirectories(dscOutputDir); + } + + var dscFilename = context.config().getOptionalValue(DESCRIPTOR_SET_FILENAME, String.class) + .orElse("descriptor_set.dsc"); + + return dscOutputDir.resolve(dscFilename).normalize(); + } + private Collection gatherDirectoriesWithImports(Path workDir, CodeGenContext context) throws CodeGenException { Config properties = context.config(); diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/build.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/build.gradle new file mode 100644 index 00000000000000..57535418f2507b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'io.quarkus' +} + +repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy' + implementation 'io.quarkus:quarkus-grpc' +} + +group 'org.acme' +version '1.0.0-SNAPSHOT' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +test { + systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/gradle.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/settings.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/settings.gradle new file mode 100644 index 00000000000000..662c07020134c5 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} +rootProject.name = 'grpc-descriptor-set-alternate-output-dir' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/java/org/acme/quarkus/sample/HelloResource.java b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/java/org/acme/quarkus/sample/HelloResource.java new file mode 100644 index 00000000000000..e5c864ff6be0e9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/java/org/acme/quarkus/sample/HelloResource.java @@ -0,0 +1,21 @@ +package org.acme.quarkus.sample; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.example.HelloMsg; + +@Path("/hello") +public class HelloResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + Integer number = HelloMsg.Status.TEST_ONE.getNumber(); + // return a thing from proto file (for devmode test) + return "hello " + number; + } +} diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/proto/hello.proto b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/proto/hello.proto new file mode 100644 index 00000000000000..4ebcaf36db77ab --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package io.quarkus.example; + +option java_multiple_files = true; +option java_package = "io.quarkus.example"; + +import "google/protobuf/timestamp.proto"; + + +message HelloMsg { + enum Status { + UNKNOWN = 0; + NOT_SERVING = 1; + TEST_ONE = 2; + } + string message = 1; + google.protobuf.Timestamp date_time = 2; + Status status = 3; +} + +service DevModeService { + rpc Check(HelloMsg) returns (HelloMsg); +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties new file mode 100644 index 00000000000000..f4576b12d2123d --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true +quarkus.generate-code.grpc.descriptor-set.name=hello.dsc +quarkus.generate-code.grpc.descriptor-set.output-dir=proto \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/build.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/build.gradle new file mode 100644 index 00000000000000..57535418f2507b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'io.quarkus' +} + +repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy' + implementation 'io.quarkus:quarkus-grpc' +} + +group 'org.acme' +version '1.0.0-SNAPSHOT' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +test { + systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/gradle.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/settings.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/settings.gradle new file mode 100644 index 00000000000000..687590ad969809 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} +rootProject.name = 'grpc-descriptor-set-alternate-output' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/java/org/acme/quarkus/sample/HelloResource.java b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/java/org/acme/quarkus/sample/HelloResource.java new file mode 100644 index 00000000000000..e5c864ff6be0e9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/java/org/acme/quarkus/sample/HelloResource.java @@ -0,0 +1,21 @@ +package org.acme.quarkus.sample; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.example.HelloMsg; + +@Path("/hello") +public class HelloResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + Integer number = HelloMsg.Status.TEST_ONE.getNumber(); + // return a thing from proto file (for devmode test) + return "hello " + number; + } +} diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/proto/hello.proto b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/proto/hello.proto new file mode 100644 index 00000000000000..4ebcaf36db77ab --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package io.quarkus.example; + +option java_multiple_files = true; +option java_package = "io.quarkus.example"; + +import "google/protobuf/timestamp.proto"; + + +message HelloMsg { + enum Status { + UNKNOWN = 0; + NOT_SERVING = 1; + TEST_ONE = 2; + } + string message = 1; + google.protobuf.Timestamp date_time = 2; + Status status = 3; +} + +service DevModeService { + rpc Check(HelloMsg) returns (HelloMsg); +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/resources/application.properties new file mode 100644 index 00000000000000..8181681cb49514 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true +quarkus.generate-code.grpc.descriptor-set.name=hello.dsc \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle new file mode 100644 index 00000000000000..57535418f2507b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'io.quarkus' +} + +repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy' + implementation 'io.quarkus:quarkus-grpc' +} + +group 'org.acme' +version '1.0.0-SNAPSHOT' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +test { + systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/gradle.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/settings.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/settings.gradle new file mode 100644 index 00000000000000..46dc110045b06e --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} +rootProject.name = 'grpc-descriptor-set' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/java/org/acme/quarkus/sample/HelloResource.java b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/java/org/acme/quarkus/sample/HelloResource.java new file mode 100644 index 00000000000000..e5c864ff6be0e9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/java/org/acme/quarkus/sample/HelloResource.java @@ -0,0 +1,21 @@ +package org.acme.quarkus.sample; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.example.HelloMsg; + +@Path("/hello") +public class HelloResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + Integer number = HelloMsg.Status.TEST_ONE.getNumber(); + // return a thing from proto file (for devmode test) + return "hello " + number; + } +} diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/proto/hello.proto b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/proto/hello.proto new file mode 100644 index 00000000000000..4ebcaf36db77ab --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package io.quarkus.example; + +option java_multiple_files = true; +option java_package = "io.quarkus.example"; + +import "google/protobuf/timestamp.proto"; + + +message HelloMsg { + enum Status { + UNKNOWN = 0; + NOT_SERVING = 1; + TEST_ONE = 2; + } + string message = 1; + google.protobuf.Timestamp date_time = 2; + Status status = 3; +} + +service DevModeService { + rpc Check(HelloMsg) returns (HelloMsg); +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/resources/application.properties new file mode 100644 index 00000000000000..b39cd85e72737a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true \ No newline at end of file diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputBuildTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputBuildTest.java new file mode 100644 index 00000000000000..e108c207370d74 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputBuildTest.java @@ -0,0 +1,27 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class GrpcDescriptorSetAlternateOutputBuildTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testGrpcDescriptorSetAlternateOutput() throws Exception { + var projectDir = getProjectDir("grpc-descriptor-set-alternate-output"); + var buildResult = runGradleWrapper(projectDir, "clean", "build"); + assertThat(BuildResult.isSuccessful(buildResult.getTasks().get(":quarkusGenerateCode"))).isTrue(); + + var expectedOutputDir = projectDir.toPath() + .resolve("build") + .resolve("classes") + .resolve("java") + .resolve("quarkus-generated-sources") + .resolve("grpc"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("hello.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputDirBuildTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputDirBuildTest.java new file mode 100644 index 00000000000000..579042e321d110 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputDirBuildTest.java @@ -0,0 +1,24 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class GrpcDescriptorSetAlternateOutputDirBuildTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testGrpcDescriptorSetAlternateOutputDir() throws Exception { + var projectDir = getProjectDir("grpc-descriptor-set-alternate-output-dir"); + var buildResult = runGradleWrapper(projectDir, "clean", "build"); + assertThat(BuildResult.isSuccessful(buildResult.getTasks().get(":quarkusGenerateCode"))).isTrue(); + + var expectedOutputDir = projectDir.toPath() + .resolve("build") + .resolve("proto"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("hello.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetBuildTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetBuildTest.java new file mode 100644 index 00000000000000..c5506309e0bc89 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetBuildTest.java @@ -0,0 +1,27 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class GrpcDescriptorSetBuildTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testGrpcDescriptorSet() throws Exception { + var projectDir = getProjectDir("grpc-descriptor-set"); + var buildResult = runGradleWrapper(projectDir, "clean", "build"); + assertThat(BuildResult.isSuccessful(buildResult.getTasks().get(":quarkusGenerateCode"))).isTrue(); + + var expectedOutputDir = projectDir.toPath() + .resolve("build") + .resolve("classes") + .resolve("java") + .resolve("quarkus-generated-sources") + .resolve("grpc"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("descriptor_set.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/pom.xml b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/pom.xml new file mode 100644 index 00000000000000..8f819511f53e69 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + quarkus-integration-test-grpc-descriptor-sets-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-descriptor-sets-alternate-output-dir + Quarkus - Integration Tests - gRPC - Descriptor Sets - Alternate Output Dir + + + + + maven-surefire-plugin + + + ${project.build.directory} + + + + + + diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java new file mode 100644 index 00000000000000..4922f5e81e09ea --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java @@ -0,0 +1,41 @@ +package io.quarkus.grpc.examples.hello; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import examples.GreeterGrpc; +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcClient; +import io.smallrye.mutiny.Uni; + +@Path("/hello") +public class HelloWorldEndpoint { + + @GrpcClient("hello") + GreeterGrpc.GreeterBlockingStub blockingHelloService; + + @GrpcClient("hello") + MutinyGreeterGrpc.MutinyGreeterStub mutinyHelloService; + + @GET + @Path("/blocking/{name}") + public String helloBlocking(@PathParam("name") String name) { + HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()); + return generateResponse(reply); + + } + + @GET + @Path("/mutiny/{name}") + public Uni helloMutiny(@PathParam("name") String name) { + return mutinyHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()) + .onItem().transform((reply) -> generateResponse(reply)); + } + + public String generateResponse(HelloReply reply) { + return String.format("%s! HelloWorldService has been called %d number of times.", reply.getMessage(), reply.getCount()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java new file mode 100644 index 00000000000000..6b13fdf54462da --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java @@ -0,0 +1,23 @@ +package io.quarkus.grpc.examples.hello; + +import java.util.concurrent.atomic.AtomicInteger; + +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcService; +import io.smallrye.mutiny.Uni; + +@GrpcService +public class HelloWorldService extends MutinyGreeterGrpc.GreeterImplBase { + + AtomicInteger counter = new AtomicInteger(); + + @Override + public Uni sayHello(HelloRequest request) { + int count = counter.incrementAndGet(); + String name = request.getName(); + return Uni.createFrom().item("Hello " + name) + .map(res -> HelloReply.newBuilder().setMessage(res).setCount(count).build()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/proto/helloworld.proto b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/proto/helloworld.proto new file mode 100644 index 00000000000000..c50ba71b3b4fa4 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/proto/helloworld.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +option java_multiple_files = true; +option java_package = "examples"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + required string name = 1; +} + +message HelloReply { + required string message = 1; + optional int32 count = 2; +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties new file mode 100644 index 00000000000000..214afae1821d51 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties @@ -0,0 +1,12 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true +quarkus.generate-code.grpc.descriptor-set.name=hello.dsc +quarkus.generate-code.grpc.descriptor-set.output-dir=proto + +quarkus.grpc.server.port=9001 + +quarkus.grpc.clients.hello.host=localhost +quarkus.grpc.clients.hello.port=9001 + +%vertx.quarkus.grpc.clients.hello.port=8081 +%vertx.quarkus.grpc.clients.hello.use-quarkus-grpc-client=true +%vertx.quarkus.grpc.server.use-separate-server=false diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java new file mode 100644 index 00000000000000..a982554fb6d758 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java @@ -0,0 +1,21 @@ +package io.quarkus.grpc.examples.hello; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +public class DescriptorSetExistsTest { + + @Test + public void descriptorSetExists() { + var expectedOutputDir = Path.of(System.getProperty("build.dir")) + .resolve("proto"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("hello.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/pom.xml b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/pom.xml new file mode 100644 index 00000000000000..91203496baddea --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + quarkus-integration-test-grpc-descriptor-sets-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-descriptor-sets-alternate-output + Quarkus - Integration Tests - gRPC - Descriptor Sets - Alternate Output + + + + + maven-surefire-plugin + + + ${project.build.directory} + + + + + + diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java new file mode 100644 index 00000000000000..4922f5e81e09ea --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java @@ -0,0 +1,41 @@ +package io.quarkus.grpc.examples.hello; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import examples.GreeterGrpc; +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcClient; +import io.smallrye.mutiny.Uni; + +@Path("/hello") +public class HelloWorldEndpoint { + + @GrpcClient("hello") + GreeterGrpc.GreeterBlockingStub blockingHelloService; + + @GrpcClient("hello") + MutinyGreeterGrpc.MutinyGreeterStub mutinyHelloService; + + @GET + @Path("/blocking/{name}") + public String helloBlocking(@PathParam("name") String name) { + HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()); + return generateResponse(reply); + + } + + @GET + @Path("/mutiny/{name}") + public Uni helloMutiny(@PathParam("name") String name) { + return mutinyHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()) + .onItem().transform((reply) -> generateResponse(reply)); + } + + public String generateResponse(HelloReply reply) { + return String.format("%s! HelloWorldService has been called %d number of times.", reply.getMessage(), reply.getCount()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java new file mode 100644 index 00000000000000..6b13fdf54462da --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java @@ -0,0 +1,23 @@ +package io.quarkus.grpc.examples.hello; + +import java.util.concurrent.atomic.AtomicInteger; + +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcService; +import io.smallrye.mutiny.Uni; + +@GrpcService +public class HelloWorldService extends MutinyGreeterGrpc.GreeterImplBase { + + AtomicInteger counter = new AtomicInteger(); + + @Override + public Uni sayHello(HelloRequest request) { + int count = counter.incrementAndGet(); + String name = request.getName(); + return Uni.createFrom().item("Hello " + name) + .map(res -> HelloReply.newBuilder().setMessage(res).setCount(count).build()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/proto/helloworld.proto b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/proto/helloworld.proto new file mode 100644 index 00000000000000..c50ba71b3b4fa4 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/proto/helloworld.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +option java_multiple_files = true; +option java_package = "examples"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + required string name = 1; +} + +message HelloReply { + required string message = 1; + optional int32 count = 2; +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/resources/application.properties b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/resources/application.properties new file mode 100644 index 00000000000000..20a1d5d62db45c --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/resources/application.properties @@ -0,0 +1,11 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true +quarkus.generate-code.grpc.descriptor-set.name=hello.dsc + +quarkus.grpc.server.port=9001 + +quarkus.grpc.clients.hello.host=localhost +quarkus.grpc.clients.hello.port=9001 + +%vertx.quarkus.grpc.clients.hello.port=8081 +%vertx.quarkus.grpc.clients.hello.use-quarkus-grpc-client=true +%vertx.quarkus.grpc.server.use-separate-server=false diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java new file mode 100644 index 00000000000000..0c1a586f0ec381 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java @@ -0,0 +1,22 @@ +package io.quarkus.grpc.examples.hello; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +public class DescriptorSetExistsTest { + + @Test + public void descriptorSetExists() { + var expectedOutputDir = Path.of(System.getProperty("build.dir")) + .resolve("generated-sources") + .resolve("grpc"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("hello.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/pom.xml b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/pom.xml new file mode 100644 index 00000000000000..cc417c76e19b9c --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + quarkus-integration-test-grpc-descriptor-sets-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-descriptor-sets-descriptor-set + Quarkus - Integration Tests - gRPC - Descriptor Sets - Descriptor Set + + + + + maven-surefire-plugin + + + ${project.build.directory} + + + + + + diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java new file mode 100644 index 00000000000000..4922f5e81e09ea --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java @@ -0,0 +1,41 @@ +package io.quarkus.grpc.examples.hello; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import examples.GreeterGrpc; +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcClient; +import io.smallrye.mutiny.Uni; + +@Path("/hello") +public class HelloWorldEndpoint { + + @GrpcClient("hello") + GreeterGrpc.GreeterBlockingStub blockingHelloService; + + @GrpcClient("hello") + MutinyGreeterGrpc.MutinyGreeterStub mutinyHelloService; + + @GET + @Path("/blocking/{name}") + public String helloBlocking(@PathParam("name") String name) { + HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()); + return generateResponse(reply); + + } + + @GET + @Path("/mutiny/{name}") + public Uni helloMutiny(@PathParam("name") String name) { + return mutinyHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()) + .onItem().transform((reply) -> generateResponse(reply)); + } + + public String generateResponse(HelloReply reply) { + return String.format("%s! HelloWorldService has been called %d number of times.", reply.getMessage(), reply.getCount()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java new file mode 100644 index 00000000000000..6b13fdf54462da --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java @@ -0,0 +1,23 @@ +package io.quarkus.grpc.examples.hello; + +import java.util.concurrent.atomic.AtomicInteger; + +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcService; +import io.smallrye.mutiny.Uni; + +@GrpcService +public class HelloWorldService extends MutinyGreeterGrpc.GreeterImplBase { + + AtomicInteger counter = new AtomicInteger(); + + @Override + public Uni sayHello(HelloRequest request) { + int count = counter.incrementAndGet(); + String name = request.getName(); + return Uni.createFrom().item("Hello " + name) + .map(res -> HelloReply.newBuilder().setMessage(res).setCount(count).build()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/proto/helloworld.proto b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/proto/helloworld.proto new file mode 100644 index 00000000000000..c50ba71b3b4fa4 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/proto/helloworld.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +option java_multiple_files = true; +option java_package = "examples"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + required string name = 1; +} + +message HelloReply { + required string message = 1; + optional int32 count = 2; +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/resources/application.properties b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/resources/application.properties new file mode 100644 index 00000000000000..b63c49d1fd2612 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/resources/application.properties @@ -0,0 +1,10 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true + +quarkus.grpc.server.port=9001 + +quarkus.grpc.clients.hello.host=localhost +quarkus.grpc.clients.hello.port=9001 + +%vertx.quarkus.grpc.clients.hello.port=8081 +%vertx.quarkus.grpc.clients.hello.use-quarkus-grpc-client=true +%vertx.quarkus.grpc.server.use-separate-server=false diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java new file mode 100644 index 00000000000000..4b64f9caccae66 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java @@ -0,0 +1,22 @@ +package io.quarkus.grpc.examples.hello; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +public class DescriptorSetExistsTest { + + @Test + public void descriptorSetExists() { + var expectedOutputDir = Path.of(System.getProperty("build.dir")) + .resolve("generated-sources") + .resolve("grpc"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("descriptor_set.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/grpc-descriptor-sets/pom.xml b/integration-tests/grpc-descriptor-sets/pom.xml new file mode 100644 index 00000000000000..45a21572d641dd --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-descriptor-sets-parent + Quarkus - Integration Tests - gRPC - Descriptor Sets - Parent + pom + + + grpc-descriptor-set + grpc-descriptor-set-alternate-output + grpc-descriptor-set-alternate-output-dir + + + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-grpc + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-test-grpc + ${project.version} + test + + + + + io.quarkus + quarkus-grpc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + generate-code + build + + + + + + + + diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index b623b32388ee67..ce18414ff57682 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -373,6 +373,7 @@ locales redis-devservices + grpc-descriptor-sets grpc-inprocess grpc-vertx grpc-tls From 337253feb85b02553ab4aec496b0537b2390f2ce Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Fri, 10 Nov 2023 10:58:22 +1100 Subject: [PATCH 27/68] Upgrade es-module-shims to 1.8.1 Signed-off-by: Phillip Kruger --- build-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-parent/pom.xml b/build-parent/pom.xml index bd06d38ef306f4..e82ebbc846621c 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -187,7 +187,7 @@ 1.7.0 5.4.3 2.1.0 - 1.8.0 + 1.8.1 2.4.0 From 0528f8c5b9527af6dbb0c875bc3293939c3cfc71 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 10 Nov 2023 10:52:45 +0200 Subject: [PATCH 28/68] Improve response filter documentation Close: #36955 --- docs/src/main/asciidoc/resteasy-reactive.adoc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index b2214898aa36b6..7d9792a76c145a 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -2219,6 +2219,8 @@ class Filters { } ---- +Such a response filter will also be called for <> exceptions. + Your filters may declare any of the following parameter types: .Filter parameters @@ -2235,7 +2237,7 @@ Your filters may declare any of the following parameter types: |A context object to access the current response |link:{jdkapi}/java/lang/Throwable.html[`Throwable`] -|Any thrown exception, or `null` (only for response filters) +|Any thrown and <> exception, or `null` (only for response filters). |=== @@ -2316,6 +2318,11 @@ Now, whenever a REST method is invoked, the request will be logged into the cons 2019-06-05 12:51:04,485 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 127.0.0.1 ---- +[NOTE] +==== +A `ContainerResponseFilter` will also be called for <> exceptions. +==== + === Readers and Writers: mapping entities and HTTP bodies [[readers-writers]] From e1a28ebac4297187b74d2aef1f407f7b78b891e1 Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Fri, 6 Oct 2023 11:25:39 +0200 Subject: [PATCH 29/68] ci: increase native tests heap memory Signed-off-by: Marc Nuri --- .github/workflows/ci-actions-incremental.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 47fa67d932ce2b..63045ac0ab9f56 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -43,7 +43,7 @@ env: # Workaround testsuite locale issue LANG: en_US.UTF-8 COMMON_MAVEN_ARGS: "-e -B --settings .github/mvn-settings.xml --fail-at-end" - NATIVE_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dquarkus.native.native-image-xmx=5g -Dnative -Dnative.surefire.skip -Dformat.skip -Dno-descriptor-tests clean install -DskipDocs" + NATIVE_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dquarkus.native.native-image-xmx=6g -Dnative -Dnative.surefire.skip -Dformat.skip -Dno-descriptor-tests clean install -DskipDocs" JVM_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dformat.skip -DskipDocs -Dquarkus.test.hang-detection-timeout=60" DB_USER: hibernate_orm_test DB_PASSWORD: hibernate_orm_test From f77a865cc39a6067ae481d6f8ee585ffb34ec1d7 Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Thu, 5 Oct 2023 18:31:01 +0200 Subject: [PATCH 30/68] deps: Bump kubernetes-client-bom from 6.8.1 to 6.9.2 Signed-off-by: Marc Nuri --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f7323a0c9aa8d6..9f70dec9adab92 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ 11 11 true - + ${env.GRAALVM_HOME} jdbc:postgresql:hibernate_orm_test @@ -69,7 +69,7 @@ 0.8.11 - 6.8.1 + 6.9.2 1.59.0 From e013c3dcfb77fa31e2b25bacc0b778bdf70bb87e Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Mon, 6 Nov 2023 11:33:06 +0100 Subject: [PATCH 31/68] deps: Bump dekorate from 4.0.0 to 4.0.3 Signed-off-by: Marc Nuri --- bom/application/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 12ba92c88e127e..9bff60d9dfac49 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -163,7 +163,7 @@ 1.7.3 0.27.0 1.6.0 - 4.0.0 + 4.0.3 3.2.0 4.2.0 3.0.2.Final @@ -3291,7 +3291,7 @@ importmap ${importmap.version}
- + biz.paluch.logging From b259b9a468bfab3fdcfe0cfd6d3584a98396b4cc Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 10 Nov 2023 11:43:14 +0100 Subject: [PATCH 32/68] Build cache - Upload quarkus-ide-launcher-999-SNAPSHOT.jar This will be used for debug. --- .github/workflows/ci-actions-incremental.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 47fa67d932ce2b..7b39d3b83b18f8 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -398,6 +398,12 @@ jobs: uses: gradle/github-actions/maven-build-scan/save@v1-beta with: job-name: "JVM Tests - JDK ${{matrix.java.name}}" + - name: Upload quarkus-ide-launcher jar + uses: actions/upload-artifact@v3 + with: + name: "quarkus-ide-launcher-999-SNAPSHOT.jar - JDK ${{matrix.java.name}}" + path: | + core/launcher/target/quarkus-ide-launcher-999-SNAPSHOT.jar maven-tests: name: Maven Tests - JDK ${{matrix.java.name}} From 56ac76941a83059ce9953ca681bbdda737037c61 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 10 Nov 2023 12:47:01 +0100 Subject: [PATCH 33/68] Revert "Unblock SmallRye Health exposed routes" This reverts commit 2c21a99aaf8fa61bbd7332f980e222ea1928fa32. --- .../deployment/SmallRyeHealthProcessor.java | 21 +++++++++++ .../runtime/SmallRyeHealthGroupHandler.java | 5 ++- .../health/runtime/SmallRyeHealthHandler.java | 5 ++- .../runtime/SmallRyeHealthHandlerBase.java | 36 ++++++++----------- .../SmallRyeIndividualHealthGroupHandler.java | 5 ++- .../runtime/SmallRyeLivenessHandler.java | 5 ++- .../runtime/SmallRyeReadinessHandler.java | 5 ++- .../runtime/SmallRyeStartupHandler.java | 5 ++- .../runtime/SmallRyeWellnessHandler.java | 5 ++- 9 files changed, 50 insertions(+), 42 deletions(-) diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java index 69d292bd816340..3899c03b691693 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -216,6 +217,7 @@ public void defineHealthRoutes(BuildProducer routes, .routeConfigKey("quarkus.smallrye-health.root-path") .handler(new SmallRyeHealthHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the liveness handler @@ -224,6 +226,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.livenessPath) .handler(new SmallRyeLivenessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the readiness handler @@ -232,14 +235,29 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.readinessPath) .handler(new SmallRyeReadinessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); + // Find all health groups + Set healthGroups = new HashSet<>(); + // with simple @HealthGroup annotations + for (AnnotationInstance healthGroupAnnotation : index.getAnnotations(HEALTH_GROUP)) { + healthGroups.add(healthGroupAnnotation.value().asString()); + } + // with @HealthGroups repeatable annotations + for (AnnotationInstance healthGroupsAnnotation : index.getAnnotations(HEALTH_GROUPS)) { + for (AnnotationInstance healthGroupAnnotation : healthGroupsAnnotation.value().asNestedArray()) { + healthGroups.add(healthGroupAnnotation.value().asString()); + } + } + // Register the health group handlers routes.produce(nonApplicationRootPathBuildItem.routeBuilder() .management("quarkus.smallrye-health.management.enabled") .nestedRoute(healthConfig.rootPath, healthConfig.groupPath) .handler(new SmallRyeHealthGroupHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); SmallRyeIndividualHealthGroupHandler handler = new SmallRyeIndividualHealthGroupHandler(); @@ -248,6 +266,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.groupPath + "/*") .handler(handler) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the wellness handler @@ -256,6 +275,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.wellnessPath) .handler(new SmallRyeWellnessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the startup handler @@ -264,6 +284,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.startupPath) .handler(new SmallRyeStartupHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java index 95b87746c1b088..84c5c6fa62d0cb 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeHealthGroupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getHealthGroupsAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getHealthGroups(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java index 6d9d33066e8fbd..6960bb284bce9b 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeHealthHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getHealthAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getHealth(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java index e9993754187690..fff1485398fbc1 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java @@ -10,11 +10,7 @@ import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; -import io.smallrye.mutiny.vertx.MutinyHelper; -import io.vertx.core.Context; import io.vertx.core.Handler; -import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerResponse; @@ -22,7 +18,7 @@ abstract class SmallRyeHealthHandlerBase implements Handler { - protected abstract Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext); + protected abstract SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext); @Override public void handle(RoutingContext ctx) { @@ -45,21 +41,19 @@ private void doHandle(RoutingContext ctx) { Arc.container().instance(CurrentIdentityAssociation.class).get().setIdentity(user.getSecurityIdentity()); } SmallRyeHealthReporter reporter = Arc.container().instance(SmallRyeHealthReporter.class).get(); - Context context = Vertx.currentContext(); - getHealth(reporter, ctx).emitOn(MutinyHelper.executor(context)) - .subscribe().with(health -> { - HttpServerResponse resp = ctx.response(); - if (health.isDown()) { - resp.setStatusCode(503); - } - resp.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8"); - Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks - try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { - reporter.reportHealth(outputStream, health); - resp.end(buffer); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + SmallRyeHealth health = getHealth(reporter, ctx); + HttpServerResponse resp = ctx.response(); + if (health.isDown()) { + resp.setStatusCode(503); + } + resp.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8"); + Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks + try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { + reporter.reportHealth(outputStream, health); + resp.end(buffer); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } + } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java index e0c7ba38744399..66f960791ad8d8 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java @@ -2,14 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeIndividualHealthGroupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { String group = ctx.normalizedPath().substring(ctx.normalizedPath().lastIndexOf("/") + 1); - return reporter.getHealthGroupAsync(group); + return reporter.getHealthGroup(group); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java index ad33e824ff3d71..a5cf3dd904cbe9 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeLivenessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getLivenessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getLiveness(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java index 18c652bd673bd7..a23a3e1f9d5383 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeReadinessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getReadinessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getReadiness(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java index cd1ae14846cc97..c450430735ecb8 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeStartupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getStartupAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getStartup(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java index e2131f51de416e..84ca3860c1caed 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeWellnessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getWellnessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getWellness(); } } From eae61d4a3b6a144f22c5c990357c2cf2b3b9c4c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:23:00 +0000 Subject: [PATCH 34/68] Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0 Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0. --- updated-dependencies: - dependency-name: org.apache.commons:commons-lang3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- independent-projects/bootstrap/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 12ba92c88e127e..3451ff9ab51eab 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -94,7 +94,7 @@ 1.7.0 2.15.2 1.0.0.Final - 3.12.0 + 3.13.0 1.16.0 1.5.1 2.8 From a448c5a5e44d1a394e61af04b0efb8e52cab2861 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 10 Nov 2023 14:53:36 +0100 Subject: [PATCH 35/68] Small adjustments for documentation related content --- .../generation/YamlMetadataGenerator.java | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java index 79048b6c42731b..f999e542440c4c 100644 --- a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java +++ b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java @@ -125,7 +125,7 @@ public void writeYamlFiles() throws StreamWriteException, DatabindException, IOE om.writeValue(targetDir.resolve("indexByType.yaml").toFile(), index); om.writeValue(targetDir.resolve("indexByFile.yaml").toFile(), metadata); - om.writeValue(targetDir.resolve("relations.yaml").toFile(), index.relationsByFile(metadata)); + om.writeValue(targetDir.resolve("relations.yaml").toFile(), index.relationsByUrl(metadata)); om.writeValue(targetDir.resolve("errorsByType.yaml").toFile(), messages); om.writeValue(targetDir.resolve("errorsByFile.yaml").toFile(), messages.allByFile()); @@ -462,8 +462,8 @@ public Map metadataByFile() { .collect(Collectors.toMap(v -> v.filename, v -> v, (o1, o2) -> o1, TreeMap::new)); } - public Map relationsByFile(Map metadataByFile) { - Map relationsByFile = new TreeMap<>(); + public Map relationsByUrl(Map metadataByFile) { + Map relationsByUrl = new TreeMap<>(); for (Entry currentMetadataEntry : metadataByFile.entrySet()) { DocRelations docRelations = new DocRelations(); @@ -482,7 +482,8 @@ public Map relationsByFile(Map metada } if (extensionMatches > 0) { docRelations.sameExtensions.add( - new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), extensionMatches)); + new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), + candidateMetadata.getType(), extensionMatches)); } int topicMatches = 0; @@ -494,16 +495,17 @@ public Map relationsByFile(Map metada if (topicMatches > 0 && (!candidateMetadata.getTopics().contains(COMPATIBILITY_TOPIC) || currentMetadataEntry.getValue().getTopics().contains(COMPATIBILITY_TOPIC))) { docRelations.sameTopics - .add(new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), topicMatches)); + .add(new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), + candidateMetadata.getType(), topicMatches)); } } if (!docRelations.isEmpty()) { - relationsByFile.put(currentMetadataEntry.getKey(), docRelations); + relationsByUrl.put(currentMetadataEntry.getValue().getUrl(), docRelations); } } - return relationsByFile; + return relationsByUrl; } // convenience @@ -649,9 +651,9 @@ public int compareTo(DocMetadata that) { @JsonInclude(value = Include.NON_EMPTY) public static class DocRelations { - Set sameTopics = new TreeSet<>(DocRelationComparator.INSTANCE); + final Set sameTopics = new TreeSet<>(DocRelationComparator.INSTANCE); - Set sameExtensions = new TreeSet<>(DocRelationComparator.INSTANCE); + final Set sameExtensions = new TreeSet<>(DocRelationComparator.INSTANCE); public Set getSameTopics() { return sameTopics; @@ -670,15 +672,18 @@ public boolean isEmpty() { @JsonInclude(value = Include.NON_EMPTY) public static class DocRelation { - String title; + final String title; + + final String url; - String url; + final String type; - int matches; + final int matches; - DocRelation(String title, String url, int matches) { + DocRelation(String title, String url, String type, int matches) { this.title = title; this.url = url; + this.type = type; this.matches = matches; } @@ -690,6 +695,10 @@ public String getUrl() { return url; } + public String getType() { + return type; + } + public int getMatches() { return matches; } From c59ee9a7ee4f75c262e1cc462820ba950407709a Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Fri, 10 Nov 2023 11:47:32 +0100 Subject: [PATCH 36/68] test: exclude large model dependencies from Kubernetes Client ITs Signed-off-by: Marc Nuri --- integration-tests/kubernetes-client/pom.xml | 10 ++++++++++ integration-tests/openshift-client/pom.xml | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/integration-tests/kubernetes-client/pom.xml b/integration-tests/kubernetes-client/pom.xml index e0d02c37608926..e42dbf99001624 100644 --- a/integration-tests/kubernetes-client/pom.xml +++ b/integration-tests/kubernetes-client/pom.xml @@ -25,6 +25,16 @@ io.quarkus quarkus-openshift-client + + + io.fabric8 + openshift-model-operator + + + io.fabric8 + openshift-model-operator-hub + + org.bouncycastle diff --git a/integration-tests/openshift-client/pom.xml b/integration-tests/openshift-client/pom.xml index a3d35c28736c74..faf03cd12a8341 100644 --- a/integration-tests/openshift-client/pom.xml +++ b/integration-tests/openshift-client/pom.xml @@ -22,6 +22,16 @@ io.quarkus quarkus-openshift-client ${project.version} + + + io.fabric8 + openshift-model-operator + + + io.fabric8 + openshift-model-operator-hub + + io.quarkus From 569ea37ab93d93f3d41d57dd733ddfa8404cf673 Mon Sep 17 00:00:00 2001 From: Falko Modler Date: Fri, 10 Nov 2023 16:26:37 +0100 Subject: [PATCH 37/68] Never register server specific providers in REST Client (fixed) --- .../deployment/RestClientReactiveProcessor.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index 04fd0a2d54494a..99f2c23cdcbcd3 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -310,18 +310,20 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, continue; } } - DotName providerDotName = providerClass.name(); + + List providerInterfaceNames = providerClass.interfaceNames(); // don't register server specific types - if (providerDotName.equals(ResteasyReactiveDotNames.CONTAINER_REQUEST_FILTER) - || providerDotName.equals(ResteasyReactiveDotNames.CONTAINER_RESPONSE_FILTER) - || providerDotName.equals(ResteasyReactiveDotNames.EXCEPTION_MAPPER)) { + if (providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_REQUEST_FILTER) + || providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_RESPONSE_FILTER) + || providerInterfaceNames.contains(ResteasyReactiveDotNames.EXCEPTION_MAPPER)) { continue; } - if (providerClass.interfaceNames().contains(ResteasyReactiveDotNames.FEATURE)) { + if (providerInterfaceNames.contains(ResteasyReactiveDotNames.FEATURE)) { continue; // features should not be automatically registered for the client, see javadoc for Feature } + DotName providerDotName = providerClass.name(); int priority = getAnnotatedPriority(index, providerDotName.toString(), Priorities.USER); constructor.invokeVirtualMethod( From 8c7876ba6248d709ecfe15d1dfa8e7a157c0e3e7 Mon Sep 17 00:00:00 2001 From: Peter Palaga Date: Wed, 8 Nov 2023 17:28:38 +0100 Subject: [PATCH 38/68] Workaround https://github.com/quarkusio/quarkus/issues/36952 alias https://github.com/jboss/jboss-parent-pom/issues/236 jboss-parent:40 still manages jdk-misc, but does not define version.jdk-misc anymore --- bom/application/pom.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 12ba92c88e127e..365bf6d0e2ca8e 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -3291,7 +3291,7 @@ importmap ${importmap.version} - + biz.paluch.logging @@ -6343,6 +6343,12 @@ test junit:junit + + + + + + org.jboss:jdk-misc From 82d1b7a5094b90162716c53d0428ddd7b5691983 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 10 Nov 2023 19:37:49 +0100 Subject: [PATCH 39/68] Revert "Make Dependabot group micro updates" This reverts commit 8833e356b50798c8de11e80afb72ab62de9a2e97. --- .github/dependabot.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 983b47e2ab8bae..293c230e2cd9cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,10 +9,6 @@ updates: open-pull-requests-limit: 6 labels: - area/dependencies - groups: - patches: - update-types: - - "patch" allow: - dependency-name: org.jboss:jboss-parent - dependency-name: org.jboss.resteasy:* From ffbf1d86c86ef65aca9b0c589722044b834be5cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 18:50:46 +0000 Subject: [PATCH 40/68] Bump org.eclipse.parsson:parsson from 1.1.4 to 1.1.5 Bumps [org.eclipse.parsson:parsson](https://github.com/eclipse-ee4j/parsson) from 1.1.4 to 1.1.5. - [Release notes](https://github.com/eclipse-ee4j/parsson/releases) - [Commits](https://github.com/eclipse-ee4j/parsson/compare/1.1.4...1.1.5) --- updated-dependencies: - dependency-name: org.eclipse.parsson:parsson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- independent-projects/resteasy-reactive/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 12ba92c88e127e..8ffb17dd79439c 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -23,7 +23,7 @@ 3.1.5 1.3.2 1 - 1.1.4 + 1.1.5 2.1.4.Final 3.0.2.Final 6.2.6.Final diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 1954af34fec2cd..99d84bb8440025 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -41,7 +41,7 @@ 2.1.2 3.1.0 4.0.1 - 1.1.4 + 1.1.5 UTF-8 11 11 From 7924dff7088aa27e06d0ac7a8f0fde345f0d51f5 Mon Sep 17 00:00:00 2001 From: David Cotton Date: Thu, 9 Nov 2023 22:51:25 +0100 Subject: [PATCH 41/68] Adds an option to download topology schema as PNG file, to ease documentation --- .../dev-ui/qwc-kafka-streams-topology.js | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js b/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js index 7e69d75f9976e2..98d40dd857ada0 100644 --- a/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js +++ b/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js @@ -1,7 +1,8 @@ import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { JsonRpc } from 'jsonrpc'; - +import { devuiState } from 'devui-state'; +import { notifier } from 'notifier'; import { Graphviz } from "@hpcc-js/wasm/graphviz.js"; import '@vaadin/details'; @@ -52,7 +53,7 @@ export class QwcKafkaStreamsTopology extends QwcHotReloadElement { Graphviz Mermaid -

${this._tabContent}

`; +

${this._tabContent}

`; } return html` this._downloadTopologyAsPng()}> + Download as PNG + `; } else { this._tabContent = html`Graph engine not started.`; } } + _downloadTopologyAsPng() { + let svgData = this.renderRoot?.querySelector('#svgSpan').getElementsByTagName("svg")[0]; + let img = new Image(svgData.width.baseVal.value, svgData.height.baseVal.value); + img.src = `data:image/svg+xml;base64,${btoa(new XMLSerializer().serializeToString(svgData))}`; + img.onload = function () { + let cnv = document.createElement('canvas'); + cnv.width = img.width; + cnv.height = img.height; + cnv.getContext("2d").drawImage(img, 0, 0); + cnv.toBlob((blob) => { + let lnk = document.createElement('a'); + lnk.href = URL.createObjectURL(blob); + lnk.download = "Topology-" + devuiState.applicationInfo.applicationName + "-" + new Date().toISOString().replace(/\D/g,'') + ".png"; + lnk.click(); + notifier.showSuccessMessage(lnk.download + " downloaded.", 'bottom-end'); + }); + } + } + _selectDetailsTab() { this._tabContent = html` From 32f13d47a96ee74cb5da49226d63e06069772012 Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Fri, 10 Nov 2023 16:08:12 -0500 Subject: [PATCH 42/68] Fix vale errors and some warnings in OidcCommonConfig --- .../oidc/common/runtime/OidcCommonConfig.java | 40 ++++++------- .../oidc/deployment/OidcBuildTimeConfig.java | 4 +- .../keycloak/DevServicesConfig.java | 18 +++--- .../io/quarkus/oidc/OidcTenantConfig.java | 57 +++++++++---------- 4 files changed, 59 insertions(+), 60 deletions(-) diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index 46527078111bae..45b1923d5d805c 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -166,13 +166,13 @@ public static enum Method { BASIC, /** - * client_secret_post: client id and secret are submitted as the 'client_id' and 'client_secret' form + * client_secret_post: client id and secret are submitted as the `client_id` and `client_secret` form * parameters. */ POST, /** - * client_secret_jwt: client id and generated JWT secret are submitted as the 'client_id' and 'client_secret' + * client_secret_jwt: client id and generated JWT secret are submitted as the `client_id` and `client_secret` * form * parameters. */ @@ -223,7 +223,7 @@ public void setSecretProvider(Provider secretProvider) { } /** - * Supports the client authentication 'client_secret_jwt' and 'private_key_jwt' methods which involve sending a JWT + * Supports the client authentication 'client_secret_jwt' and `private_key_jwt` methods which involve sending a JWT * token * assertion signed with either a client secret or private key. * @@ -252,13 +252,13 @@ public static class Jwt { public Optional keyFile = Optional.empty(); /** - * If provided, indicates that JWT is signed using a private key from a key store + * If provided, indicates that JWT is signed using a private key from a keystore */ @ConfigItem public Optional keyStoreFile = Optional.empty(); /** - * A parameter to specify the password of the key store file. + * A parameter to specify the password of the keystore file. */ @ConfigItem public Optional keyStorePassword; @@ -289,7 +289,7 @@ public static class Jwt { public Optional tokenKeyId = Optional.empty(); /** - * Issuer of the signing key added as a JWT 'iss' claim (default: client id) + * Issuer of the signing key added as a JWT `iss` claim (default: client id) */ @ConfigItem public Optional issuer = Optional.empty(); @@ -441,41 +441,41 @@ public enum Verification { } /** - * Certificate validation and hostname verification, which can be one of the following values from enum - * {@link Verification}. Default is required. + * Certificate validation and hostname verification, which can be one of the following {@link Verification} values. + * Default is required. */ @ConfigItem public Optional verification = Optional.empty(); /** - * An optional key store which holds the certificate information instead of specifying separate files. + * An optional keystore which holds the certificate information instead of specifying separate files. */ @ConfigItem public Optional keyStoreFile = Optional.empty(); /** - * An optional parameter to specify type of the key store file. If not given, the type is automatically detected + * An optional parameter to specify type of the keystore file. If not given, the type is automatically detected * based on the file name. */ @ConfigItem public Optional keyStoreFileType = Optional.empty(); /** - * An optional parameter to specify a provider of the key store file. If not given, the provider is automatically + * An optional parameter to specify a provider of the keystore file. If not given, the provider is automatically * detected - * based on the key store file type. + * based on the keystore file type. */ @ConfigItem public Optional keyStoreProvider; /** - * A parameter to specify the password of the key store file. If not given, the default ("password") is used. + * A parameter to specify the password of the keystore file. If not given, the default ("password") is used. */ @ConfigItem public Optional keyStorePassword; /** - * An optional parameter to select a specific key in the key store. When SNI is disabled, if the key store contains + * An optional parameter to select a specific key in the keystore. When SNI is disabled, if the keystore contains * multiple * keys and no alias is specified, the behavior is undefined. */ @@ -489,34 +489,34 @@ public enum Verification { public Optional keyStoreKeyPassword = Optional.empty(); /** - * An optional trust store which holds the certificate information of the certificates to trust + * An optional truststore which holds the certificate information of the certificates to trust */ @ConfigItem public Optional trustStoreFile = Optional.empty(); /** - * A parameter to specify the password of the trust store file. + * A parameter to specify the password of the truststore file. */ @ConfigItem public Optional trustStorePassword = Optional.empty(); /** - * A parameter to specify the alias of the trust store certificate. + * A parameter to specify the alias of the truststore certificate. */ @ConfigItem public Optional trustStoreCertAlias = Optional.empty(); /** - * An optional parameter to specify type of the trust store file. If not given, the type is automatically detected + * An optional parameter to specify type of the truststore file. If not given, the type is automatically detected * based on the file name. */ @ConfigItem public Optional trustStoreFileType = Optional.empty(); /** - * An optional parameter to specify a provider of the trust store file. If not given, the provider is automatically + * An optional parameter to specify a provider of the truststore file. If not given, the provider is automatically * detected - * based on the trust store file type. + * based on the truststore file type. */ @ConfigItem public Optional trustStoreProvider; 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 b9e2edd863702d..2a25611394c301 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 @@ -22,8 +22,8 @@ public class OidcBuildTimeConfig { public DevUiConfig devui; /** * Enable the registration of the Default TokenIntrospection and UserInfo Cache implementation bean. - * Note it only allows to use the default implementation, one needs to configure it in order to activate it, - * please see {@link OidcConfig#tokenCache}. + * Note: This only enables the default implementation. It requires configuration to be activated. + * See {@link OidcConfig#tokenCache}. */ @ConfigItem(defaultValue = "true") public boolean defaultTokenCacheEnabled; diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 624675b4fd05c9..05a69f6df10150 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -23,11 +23,11 @@ public class DevServicesConfig { public boolean enabled = true; /** - * The container image name to use, for container based DevServices providers. + * The container image name to use, for container-based DevServices providers. * * Image with a Quarkus based distribution is used by default. * Image with a WildFly based distribution can be selected instead, for example: - * 'quay.io/keycloak/keycloak:19.0.3-legacy'. + * `quay.io/keycloak/keycloak:19.0.3-legacy`. *

* Note Keycloak Quarkus and Keycloak WildFly images are initialized differently. * By default, Dev Services for Keycloak will assume it is a Keycloak Quarkus image if the image version does not end with a @@ -106,17 +106,17 @@ public class DevServicesConfig { /** * The Keycloak realm name. - * This property will be used to create the realm if the realm file pointed to by the 'realm-path' property does not exist, - * default value is 'quarkus' in this case. - * If the realm file pointed to by the 'realm-path' property exists then it is still recommended to set this property - * for Dev Services for Keycloak to avoid parsing the realm file in order to determine the realm name. + * This property will be used to create the realm if the realm file pointed to by the `realm-path` property does not exist, + * default value is `quarkus` in this case. + * If the realm file pointed to by the `realm-path` property exists then it is still recommended to set this property + * for Dev Services for Keycloak to avoid parsing the realm file to determine the realm name. * */ @ConfigItem public Optional realmName; /** - * Indicates if the Keycloak realm has to be created when the realm file pointed to by the 'realm-path' property does not + * Indicates if the Keycloak realm has to be created when the realm file pointed to by the `realm-path` property does not * exist. * * Disable it if you'd like to create a realm using Keycloak Administration Console @@ -128,7 +128,7 @@ public class DevServicesConfig { /** * The Keycloak users map containing the username and password pairs. * If this map is empty then two users, 'alice' and 'bob' with the passwords matching their names will be created. - * This property will be used to create the Keycloak users if the realm file pointed to by the 'realm-path' property does + * This property will be used to create the Keycloak users if the realm file pointed to by the `realm-path` property does * not exist. */ @ConfigItem @@ -138,7 +138,7 @@ public class DevServicesConfig { * The Keycloak user roles. * If this map is empty then a user named 'alice' will get 'admin' and 'user' roles and all other users will get a 'user' * role. - * This property will be used to create the Keycloak roles if the realm file pointed to by the 'realm-path' property does + * This property will be used to create the Keycloak roles if the realm file pointed to by the `realm-path` property does * not exist. */ @ConfigItem diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 48d25c15adfa43..db4eea48e8d00d 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -40,7 +40,7 @@ public class OidcTenantConfig extends OidcCommonConfig { public boolean tenantEnabled = true; /** - * The application type, which can be one of the following values from enum {@link ApplicationType}. + * The application type, which can be one of the following {@link ApplicationType} values. */ @ConfigItem(defaultValueDocumentation = "service") public Optional applicationType = Optional.empty(); @@ -54,9 +54,9 @@ public class OidcTenantConfig extends OidcCommonConfig { public Optional authorizationPath = Optional.empty(); /** - * Relative path or absolute URL of the OIDC userinfo endpoint. + * Relative path or absolute URL of the OIDC UserInfo endpoint. * This property must only be set for the 'web-app' applications if OIDC discovery is disabled - * and 'authentication.user-info-required' property is enabled. + * and `authentication.user-info-required` property is enabled. * This property will be ignored if the discovery is enabled. */ @ConfigItem @@ -189,7 +189,7 @@ public void setIncludeClientId(boolean includeClientId) { /** * Allow caching the token introspection data. * Note enabling this property does not enable the cache itself but only permits to cache the token introspection - * for a given tenant. If the default token cache can be used then please see {@link OidcConfig.TokenCache} how to enable + * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable * it. */ @ConfigItem(defaultValue = "true") @@ -198,7 +198,7 @@ public void setIncludeClientId(boolean includeClientId) { /** * Allow caching the user info data. * Note enabling this property does not enable the cache itself but only permits to cache the user info data - * for a given tenant. If the default token cache can be used then please see {@link OidcConfig.TokenCache} how to enable + * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable * it. */ @ConfigItem(defaultValue = "true") @@ -689,7 +689,7 @@ public enum CookieSameSite { */ public enum ResponseMode { /** - * Authorization response parameters are encoded in the query string added to the redirect_uri + * Authorization response parameters are encoded in the query string added to the `redirect_uri` */ QUERY, @@ -707,19 +707,19 @@ public enum ResponseMode { public Optional responseMode = Optional.empty(); /** - * Relative path for calculating a "redirect_uri" query parameter. + * Relative path for calculating a `redirect_uri` query parameter. * It has to start from a forward slash and will be appended to the request URI's host and port. - * For example, if the current request URI is 'https://localhost:8080/service' then a 'redirect_uri' parameter + * For example, if the current request URI is 'https://localhost:8080/service' then a `redirect_uri` parameter * will be set to 'https://localhost:8080/' if this property is set to '/' and be the same as the request URI * if this property has not been configured. * Note the original request URI will be restored after the user has authenticated if 'restorePathAfterRedirect' is set - * to 'true'. + * to `true`. */ @ConfigItem public Optional redirectPath = Optional.empty(); /** - * If this property is set to 'true' then the original request URI which was used before + * If this property is set to `true` then the original request URI which was used before * the authentication will be restored after the user has been redirected back to the application. * * Note if `redirectPath` property is not set, the original request URI will be restored even if this property is @@ -737,8 +737,8 @@ public enum ResponseMode { /** * Relative path to the public endpoint which will process the error response from the OIDC authorization endpoint. - * If the user authentication has failed then the OIDC provider will return an 'error' and an optional - * 'error_description' + * If the user authentication has failed then the OIDC provider will return an `error` and an optional + * `error_description` * parameters, instead of the expected authorization 'code'. * * If this property is set then the user will be redirected to the endpoint which can return a user-friendly @@ -769,7 +769,7 @@ public enum ResponseMode { public boolean verifyAccessToken; /** - * Force 'https' as the 'redirect_uri' parameter scheme when running behind an SSL terminating reverse proxy. + * Force 'https' as the `redirect_uri` parameter scheme when running behind an SSL/TLS terminating reverse proxy. * This property, if enabled, will also affect the logout `post_logout_redirect_uri` and the local redirect requests. */ @ConfigItem(defaultValueDocumentation = "false") @@ -791,7 +791,7 @@ public enum ResponseMode { public boolean nonceRequired = false; /** - * Add the 'openid' scope automatically to the list of scopes. This is required for OpenId Connect providers + * Add the `openid` scope automatically to the list of scopes. This is required for OpenId Connect providers * but will not work for OAuth2 providers such as Twitter OAuth2 which does not accept that scope and throws an error. */ @ConfigItem(defaultValueDocumentation = "true") @@ -811,8 +811,8 @@ public enum ResponseMode { public Optional> forwardParams = Optional.empty(); /** - * If enabled the state, session and post logout cookies will have their 'secure' parameter set to 'true' - * when HTTP is used. It may be necessary when running behind an SSL terminating reverse proxy. + * If enabled the state, session and post logout cookies will have their 'secure' parameter set to `true` + * when HTTP is used. It may be necessary when running behind an SSL/TLS terminating reverse proxy. * The cookies will always be secure if HTTPS is used even if this property is set to false. */ @ConfigItem(defaultValue = "false") @@ -820,8 +820,8 @@ public enum ResponseMode { /** * Cookie name suffix. - * For example, a session cookie name for the default OIDC tenant is 'q_session' but can be changed to 'q_session_test' - * if this property is set to 'test'. + * For example, a session cookie name for the default OIDC tenant is `q_session` but can be changed to `q_session_test` + * if this property is set to `test`. */ @ConfigItem public Optional cookieSuffix = Optional.empty(); @@ -861,8 +861,7 @@ public enum ResponseMode { * However, if multiple authentications are attempted from the same browser, for example, from the different * browser tabs, then the currently available state cookie may represent the authentication flow * initiated from another tab and not related to the current request. - * Disable this property if you would like to avoid supporting multiple authorization code flows running in the same - * browser. + * Disable this property to permit only a single authorization code flow in the same browser. * */ @ConfigItem(defaultValue = "true") @@ -886,14 +885,14 @@ public enum ResponseMode { * with {@link #redirectPath} may be needed to avoid such errors. *

* However, setting this property to `false` may help if the above options are not suitable. - * It will cause a new authentication redirect to OpenId Connect provider. Please be aware doing so may increase the + * It will cause a new authentication redirect to OpenId Connect provider. Doing so may increase the * risk of browser redirect loops. */ @ConfigItem(defaultValue = "false") public boolean failOnMissingStateParam = false; /** - * If this property is set to 'true' then an OIDC UserInfo endpoint will be called. + * If this property is set to `true` then an OIDC UserInfo endpoint will be called. * This property will be enabled if `quarkus.oidc.roles.source` is `userinfo` * or `quarkus.oidc.token.verify-access-token-with-user-info` is `true` * or `quarkus.oidc.authentication.id-token-required` is set to `false`, @@ -906,7 +905,7 @@ public enum ResponseMode { * Session age extension in minutes. * The user session age property is set to the value of the ID token life-span by default and * the user will be redirected to the OIDC provider to re-authenticate once the session has expired. - * If this property is set to a non-zero value then the expired ID token can be refreshed before + * If this property is set to a non-zero value, then the expired ID token can be refreshed before * the session has expired. * This property will be ignored if the `token.refresh-expired` property has not been enabled. */ @@ -914,7 +913,7 @@ public enum ResponseMode { public Duration sessionAgeExtension = Duration.ofMinutes(5); /** - * If this property is set to 'true' then a normal 302 redirect response will be returned + * If this property is set to `true` then a normal 302 redirect response will be returned * if the request was initiated via JavaScript API such as XMLHttpRequest or Fetch and the current user needs to be * (re)authenticated which may not be desirable for Single-page applications (SPA) since * it automatically following the redirect may not work given that OIDC authorization endpoints typically do not support @@ -1276,7 +1275,7 @@ public static Token fromAudience(String... audience) { } /** - * Expected issuer 'iss' claim value. + * Expected issuer `iss` claim value. * Note this property overrides the `issuer` property which may be set in OpenId Connect provider's well-known * configuration. * If the `iss` claim value varies depending on the host/IP address or tenant id of the provider then you may skip the @@ -1356,7 +1355,7 @@ public static Token fromAudience(String... audience) { public Optional age = Optional.empty(); /** - * Name of the claim which contains a principal name. By default, the 'upn', 'preferred_username' and `sub` claims are + * Name of the claim which contains a principal name. By default, the `upn`, `preferred_username` and `sub` claims are * checked. */ @ConfigItem @@ -1418,7 +1417,7 @@ public static Token fromAudience(String... audience) { * the providers may not control the private decryption keys. * In such cases set this property to point to the file containing the decryption private key in * PEM or JSON Web Key (JWK) format. - * Note that if a 'private_key_jwt' client authentication method is used then the private key + * Note that if a `private_key_jwt` client authentication method is used then the private key * which is used to sign client authentication JWT tokens will be used to try to decrypt an encrypted ID token * if this property is not set. */ @@ -1428,7 +1427,7 @@ public static Token fromAudience(String... audience) { /** * Allow the remote introspection of JWT tokens when no matching JWK key is available. * - * Note this property is set to 'true' by default for backward-compatibility reasons and will be set to `false` + * Note this property is set to `true` by default for backward-compatibility reasons and will be set to `false` * instead in one of the next releases. * * Also note this property will be ignored if JWK endpoint URI is not available and introspecting the tokens is @@ -1627,7 +1626,7 @@ public void setSubjectRequired(boolean subjectRequired) { public static enum ApplicationType { /** - * A {@code WEB_APP} is a client that serves pages, usually a frontend application. For this type of client the + * A {@code WEB_APP} is a client that serves pages, usually a front-end application. For this type of client the * Authorization Code Flow is defined as the preferred method for authenticating users. */ WEB_APP, From 42a028ae36e2dda57a7b8558088e9d70d26ea513 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira da Silva Date: Fri, 10 Nov 2023 18:41:03 -0300 Subject: [PATCH 43/68] Address CVE-2023-21971 present in MySQL connector Closes #37018 Signed-off-by: Bruno Oliveira da Silva --- 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 12ba92c88e127e..c41f9fbf11a2d9 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -129,7 +129,7 @@ 2.2.224 42.6.0 3.2.0 - 8.0.30 + 8.0.33 12.4.0.jre11 1.6.7 23.3.0.23.09 From 420fca375f8115c2546f8ac8b12124023151b707 Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Fri, 10 Nov 2023 17:08:36 -0500 Subject: [PATCH 44/68] Fix vale errors and some warnings in the "OpenID Connect (OIDC) and OAuth2 Client and Filters Reference Guide" --- ...urity-openid-connect-client-reference.adoc | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 17dcb5a8911128..692575db26ef7f 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -16,7 +16,7 @@ This reference guide explains how to use: The access tokens managed by these extensions can be used as HTTP Authorization Bearer tokens to access the remote services. -Please also see xref:security-openid-connect-client.adoc[OpenID Connect Client and Token Propagation Quickstart]. +Also see xref:security-openid-connect-client.adoc[OpenID Connect Client and Token Propagation Quickstart]. == OidcClient @@ -47,7 +47,7 @@ quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus `OidcClient` will discover that the token endpoint URL is `http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens`. -Alternatively, if the discovery endpoint is not available or you would like to save on the discovery endpoint round-trip, you can disable the discovery and configure the token endpoint address with a relative path value, for example: +Alternatively, if the discovery endpoint is not available or you want to save on the discovery endpoint round-trip, you can disable the discovery and configure the token endpoint address with a relative path value, for example: [source, properties] ---- @@ -64,7 +64,7 @@ A more compact way to configure the token endpoint URL without the discovery is quarkus.oidc-client.token-path=http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens ---- -Setting 'quarkus.oidc-client.auth-server-url' and 'quarkus.oidc-client.discovery-enabled' is not required in this case. +Setting `quarkus.oidc-client.auth-server-url` and `quarkus.oidc-client.discovery-enabled` is not required in this case. === Supported Token Grants @@ -111,7 +111,7 @@ It can be further customized using a `quarkus.oidc-client.grant-options.password ==== Other Grants -`OidcClient` can also help with acquiring the tokens using the grants which require some extra input parameters which can not be captured in the configuration. These grants are `refresh_token` (with the external refresh token), `authorization_code`, as well as two grants which can be used to exchange the current access token, `urn:ietf:params:oauth:grant-type:token-exchange` and `urn:ietf:params:oauth:grant-type:jwt-bearer`. +`OidcClient` can also help with acquiring the tokens using the grants which require some extra input parameters which cannot be captured in the configuration. These grants are `refresh_token` (with the external refresh token), `authorization_code`, as well as two grants which can be used to exchange the current access token, `urn:ietf:params:oauth:grant-type:token-exchange` and `urn:ietf:params:oauth:grant-type:jwt-bearer`. Using the `refresh_token` grant which uses an out-of-band refresh token to acquire a new set of tokens will be required if the existing refresh token has been posted to the current Quarkus endpoint for it to acquire the access token. In this case `OidcClient` needs to be configured as follows: @@ -125,7 +125,7 @@ quarkus.oidc-client.grant.type=refresh and then you can use `OidcClient.refreshTokens` method with a provided refresh token to get the access token. -Using the `urn:ietf:params:oauth:grant-type:token-exchange` or `urn:ietf:params:oauth:grant-type:jwt-bearer` grants may be required if you are building a complex microservices application and would like to avoid the same `Bearer` token be propagated to and used by more than one service. Please see <> and <> for more details. +Using the `urn:ietf:params:oauth:grant-type:token-exchange` or `urn:ietf:params:oauth:grant-type:jwt-bearer` grants might be required if you are building a complex microservices application and want to avoid the same `Bearer` token be propagated to and used by more than one service. See <> and <> for more details. Using `OidcClient` to support the `authorization code` grant might be required if for some reason you cannot use the xref:security-oidc-code-flow-authentication.adoc[Quarkus OIDC extension] to support Authorization Code Flow. If there is a very good reason for you to implement Authorization Code Flow then you can configure `OidcClient` as follows: @@ -153,7 +153,7 @@ and then you can use `OidcClient.accessTokens` method accepting a Map of extra p ==== Grant scopes -You may need to request that a specific set of scopes is associated with an issued access token. +You might need to request that a specific set of scopes is associated with an issued access token. Use a dedicated `quarkus.oidc-client.scopes` list property, for example: `quarkus.oidc-client.scopes=email,phone` === Use OidcClient directly @@ -215,7 +215,7 @@ public class OidcClientResource { @GET public String getResponse() { - // Get the access token which may have been refreshed. + // Get the access token, which might have been refreshed. String accessToken = tokens.getAccessToken(); // Use the access token to configure MP RestClient Authorization header/etc } @@ -395,7 +395,7 @@ It works similarly to the way `OidcClientRequestFilter` does (see < ---- -Write Wiremock based `QuarkusTestResourceLifecycleManager`, for example: +Write a Wiremock-based `QuarkusTestResourceLifecycleManager`, for example: [source, java] ---- package io.quarkus.it.keycloak; @@ -831,7 +831,7 @@ public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycl } ---- -Prepare the REST test endpoints, you can have the test frontend endpoint which uses the injected MP REST client with a registered OidcClient filter to invoke on the downstream endpoint which echoes the token back, for example, see the `integration-tests/oidc-client-wiremock` in the `main` Quarkus repository. +Prepare the REST test endpoints. You can have the test front-end endpoint, which uses the injected MP REST client with a registered OidcClient filter, call the downstream endpoint. This endpoint echoes the token back. For example, see the `integration-tests/oidc-client-wiremock` in the `main` Quarkus repository. Set `application.properties`, for example: @@ -856,7 +856,7 @@ If you work with Keycloak then you can use the same approach as described in the === How to check the errors in the logs -Please enable `io.quarkus.oidc.client.runtime.OidcClientImpl` `TRACE` level logging to see more details about the token acquisition and refresh errors: +Enable `io.quarkus.oidc.client.runtime.OidcClientImpl` `TRACE` level logging to see more details about the token acquisition and refresh errors: [source, properties] ---- @@ -864,7 +864,7 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE ---- -Please enable `io.quarkus.oidc.client.runtime.OidcClientRecorder` `TRACE` level logging to see more details about the OidcClient initialization errors: +Enable `io.quarkus.oidc.client.runtime.OidcClientRecorder` `TRACE` level logging to see more details about the OidcClient initialization errors: [source, properties] ---- @@ -967,7 +967,7 @@ quarkus.oidc-token-propagation.exchange-token=true Note `AccessTokenRequestReactiveFilter` will use `OidcClient` to exchange the current token, and you can use `quarkus.oidc-client.grant-options.exchange` to set the additional exchange properties expected by your OpenID Connect Provider. -If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exhange the current token then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: +If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exchange the current token then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: [source,properties] ---- @@ -995,7 +995,7 @@ When you need to propagate the current Authorization Code Flow access token then However, the direct end to end Bearer token propagation should be avoided if possible. For example, `Client -> Service A -> Service B` where `Service B` receives a token sent by `Client` to `Service A`. In such cases `Service B` will not be able to distinguish if the token came from `Service A` or from `Client` directly. For `Service B` to verify the token came from `Service A` it should be able to assert a new issuer and audience claims. -Additionally, a complex application may need to exchange or update the tokens before propagating them. For example, the access context might be different when `Service A` is accessing `Service B`. In this case, `Service A` might be granted a narrow or a completely different set of scopes to access `Service B`. +Additionally, a complex application might need to exchange or update the tokens before propagating them. For example, the access context might be different when `Service A` is accessing `Service B`. In this case, `Service A` might be granted a narrow or a completely different set of scopes to access `Service B`. The following sections show how `AccessTokenRequestFilter` and `JsonWebTokenRequestFilter` can help. @@ -1054,7 +1054,7 @@ quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange quarkus.oidc-token-propagation.exchange-token=true ---- -If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exhange the current token then you can configure `AccessTokenRequestFilter` to exchange the token like this: +If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exchange the current token then you can configure `AccessTokenRequestFilter` to exchange the token like this: [source,properties] ---- @@ -1131,13 +1131,13 @@ smallrye.jwt.new-token.audience=http://downstream-resource smallrye.jwt.new-token.override-matching-claims=true ---- -As already noted above, please use `AccessTokenRequestFilter` if you work with Keycloak or OpenID Connect Provider which supports a Token Exchange protocol. +As already noted above, use `AccessTokenRequestFilter` if you work with Keycloak or OpenID Connect Provider which supports a Token Exchange protocol. [[integration-testing-token-propagation]] === Testing You can generate the tokens as described in xref:security-oidc-bearer-token-authentication.adoc#integration-testing[OpenID Connect Bearer Token Integration testing] section. -Prepare the REST test endpoints, you can have the test frontend endpoint which uses the injected MP REST client with a registered token propagation filter to invoke on the downstream endpoint, for example, see the `integration-tests/oidc-token-propagation` in the `main` Quarkus repository. +Prepare the REST test endpoints. You can have the test front-end endpoint, which uses the injected MP REST client with a registered token propagation filter, call the downstream endpoint. For example, see the `integration-tests/oidc-token-propagation` in the `main` Quarkus repository. [[reactive-token-propagation]] == Token Propagation Reactive @@ -1155,7 +1155,7 @@ Add the following Maven Dependency: The `quarkus-oidc-token-propagation-reactive` extension provides `io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter` which can be used to propagate the current `Bearer` or `Authorization Code Flow` access tokens. The `quarkus-oidc-token-propagation-reactive` extension (as opposed to the non-reactive `quarkus-oidc-token-propagation` extension) does not currently support the exchanging or resigning the tokens before the propagation. -However, these features may be added in the future. +However, these features might be added in the future. [[oidc-client-graphql-client]] == GraphQL client integration @@ -1186,7 +1186,7 @@ quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.method=POS NOTE: If you don't specify the `quarkus.oidc-client-graphql.client-name` property, GraphQL clients will use the default OIDC client (without an explicit name). -Specifically for typesafe GraphQL clients, you can override this on a +Specifically for type-safe GraphQL clients, you can override this on a per-client basis by annotating the `GraphQLClientApi` interface with `@io.quarkus.oidc.client.filter.OidcClientFilter`. For example: From b08cf2f100a24b7b7225756d558303236f713846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sat, 11 Nov 2023 01:04:55 +0100 Subject: [PATCH 45/68] Move form auth and basic realm to runtime --- .../deployment/HttpSecurityProcessor.java | 27 ++-- .../vertx/http/runtime/AuthConfig.java | 6 - .../vertx/http/runtime/AuthRuntimeConfig.java | 13 ++ .../vertx/http/runtime/FormAuthConfig.java | 110 ---------------- .../http/runtime/FormAuthRuntimeConfig.java | 122 ++++++++++++++++++ .../BasicAuthenticationMechanism.java | 40 ++---- .../security/FormAuthenticationMechanism.java | 26 ++-- .../security/HttpSecurityRecorder.java | 11 -- 8 files changed, 171 insertions(+), 184 deletions(-) create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index fb34fd418ef281..1548509f4c05f2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.deployment; import static io.quarkus.arc.processor.DotNames.APPLICATION_SCOPED; +import static io.quarkus.arc.processor.DotNames.DEFAULT_BEAN; import static io.quarkus.arc.processor.DotNames.SINGLETON; import java.util.List; @@ -11,12 +12,14 @@ import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Singleton; +import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; @@ -30,7 +33,6 @@ import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder; @@ -42,6 +44,8 @@ public class HttpSecurityProcessor { + private static final DotName BASIC_AUTH_MECH_NAME = DotName.createSimple(BasicAuthenticationMechanism.class); + @Record(ExecutionTime.STATIC_INIT) @BuildStep void produceNamedHttpSecurityPolicies(List httpSecurityPolicyBuildItems, @@ -79,26 +83,23 @@ AdditionalBeanBuildItem initMtlsClientAuth(HttpBuildTimeConfig buildTimeConfig) } @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) - @Record(ExecutionTime.STATIC_INIT) - SyntheticBeanBuildItem initBasicAuth( - HttpSecurityRecorder recorder, - HttpBuildTimeConfig buildTimeConfig, + AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, + BuildProducer annotationsTransformerProducer, BuildProducer securityInformationProducer) { - SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem - .configure(BasicAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .scope(Singleton.class) - .supplier(recorder.setupBasicAuth(buildTimeConfig)); + if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) && !buildTimeConfig.auth.basic.orElse(false)) { //if not explicitly enabled we make this a default bean, so it is the fallback if nothing else is defined - configurator.defaultBean(); + annotationsTransformerProducer.produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer + .appliedToClass() + .whenClass(cl -> BASIC_AUTH_MECH_NAME.equals(cl.name())) + .thenTransform(t -> t.add(DEFAULT_BEAN)))); if (buildTimeConfig.auth.basic.isPresent() && buildTimeConfig.auth.basic.get()) { securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } } - return configurator.done(); + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(BasicAuthenticationMechanism.class).build(); } public static boolean applicationBasicAuthRequired(HttpBuildTimeConfig buildTimeConfig, diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java index 52b9017a38daaf..e4ff3dec29715a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java @@ -25,12 +25,6 @@ public class AuthConfig { @ConfigItem public FormAuthConfig form; - /** - * The authentication realm - */ - @ConfigItem - public Optional realm; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java index eee0b3f84d8970..8552e30ef6b6bb 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.runtime; import java.util.Map; +import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -22,4 +23,16 @@ public class AuthRuntimeConfig { */ @ConfigItem(name = "policy") public Map rolePolicy; + + /** + * The authentication realm + */ + @ConfigItem + public Optional realm; + + /** + * Form Auth config + */ + @ConfigItem + public FormAuthRuntimeConfig form; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java index e08e6595129ba6..2e6d38250c8da3 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java @@ -1,8 +1,5 @@ package io.quarkus.vertx.http.runtime; -import java.time.Duration; -import java.util.Optional; - import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -11,14 +8,6 @@ */ @ConfigGroup public class FormAuthConfig { - /** - * SameSite attribute values for the session and location cookies. - */ - public enum CookieSameSite { - STRICT, - LAX, - NONE - } /** * If form authentication is enabled. @@ -26,109 +15,10 @@ public enum CookieSameSite { @ConfigItem public boolean enabled; - /** - * The login page. Redirect to login page can be disabled by setting `quarkus.http.auth.form.login-page=`. - */ - @ConfigItem(defaultValue = "/login.html") - public Optional loginPage; - /** * The post location. */ @ConfigItem(defaultValue = "/j_security_check") public String postLocation; - /** - * The username field name. - */ - @ConfigItem(defaultValue = "j_username") - public String usernameParameter; - - /** - * The password field name. - */ - @ConfigItem(defaultValue = "j_password") - public String passwordParameter; - - /** - * The error page. Redirect to error page can be disabled by setting `quarkus.http.auth.form.error-page=`. - */ - @ConfigItem(defaultValue = "/error.html") - public Optional errorPage; - - /** - * The landing page to redirect to if there is no saved page to redirect back to. - * Redirect to landing page can be disabled by setting `quarkus.http.auth.form.landing-page=`. - */ - @ConfigItem(defaultValue = "/index.html") - public Optional landingPage; - - /** - * Option to disable redirect to landingPage if there is no saved page to redirect back to. Form Auth POST is followed - * by redirect to landingPage by default. - * - * @deprecated redirect to landingPage can be disabled by removing default landing page - * (via `quarkus.http.auth.form.landing-page=`). Quarkus will ignore this configuration property - * if there is no landing page. - */ - @ConfigItem(defaultValue = "true") - @Deprecated - public boolean redirectAfterLogin; - - /** - * Option to control the name of the cookie used to redirect the user back - * to where he wants to get access to. - */ - @ConfigItem(defaultValue = "quarkus-redirect-location") - public String locationCookie; - - /** - * The inactivity (idle) timeout - * - * When inactivity timeout is reached, cookie is not renewed and a new login is enforced. - */ - @ConfigItem(defaultValue = "PT30M") - public Duration timeout; - - /** - * How old a cookie can get before it will be replaced with a new cookie with an updated timeout, also - * referred to as "renewal-timeout". - * - * Note that smaller values will result in slightly more server load (as new encrypted cookies will be - * generated more often), however larger values affect the inactivity timeout as the timeout is set - * when a cookie is generated. - * - * For example if this is set to 10 minutes, and the inactivity timeout is 30m, if a users last request - * is when the cookie is 9m old then the actual timeout will happen 21m after the last request, as the timeout - * is only refreshed when a new cookie is generated. - * - * In other words no timeout is tracked on the server side; the timestamp is encoded and encrypted in the cookie itself, - * and it is decrypted and parsed with each request. - */ - @ConfigItem(defaultValue = "PT1M") - public Duration newCookieInterval; - - /** - * The cookie that is used to store the persistent session - */ - @ConfigItem(defaultValue = "quarkus-credential") - public String cookieName; - - /** - * The cookie path for the session and location cookies. - */ - @ConfigItem(defaultValue = "/") - public Optional cookiePath = Optional.of("/"); - - /** - * Set the HttpOnly attribute to prevent access to the cookie via JavaScript. - */ - @ConfigItem(defaultValue = "false") - public boolean httpOnlyCookie; - - /** - * SameSite attribute for the session and location cookies. - */ - @ConfigItem(defaultValue = "strict") - public CookieSameSite cookieSameSite = CookieSameSite.STRICT; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java new file mode 100644 index 00000000000000..d4152fe4a9b133 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java @@ -0,0 +1,122 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * config for the form authentication mechanism + */ +@ConfigGroup +public class FormAuthRuntimeConfig { + /** + * SameSite attribute values for the session and location cookies. + */ + public enum CookieSameSite { + STRICT, + LAX, + NONE + } + + /** + * The login page. Redirect to login page can be disabled by setting `quarkus.http.auth.form.login-page=`. + */ + @ConfigItem(defaultValue = "/login.html") + public Optional loginPage; + + /** + * The username field name. + */ + @ConfigItem(defaultValue = "j_username") + public String usernameParameter; + + /** + * The password field name. + */ + @ConfigItem(defaultValue = "j_password") + public String passwordParameter; + + /** + * The error page. Redirect to error page can be disabled by setting `quarkus.http.auth.form.error-page=`. + */ + @ConfigItem(defaultValue = "/error.html") + public Optional errorPage; + + /** + * The landing page to redirect to if there is no saved page to redirect back to. + * Redirect to landing page can be disabled by setting `quarkus.http.auth.form.landing-page=`. + */ + @ConfigItem(defaultValue = "/index.html") + public Optional landingPage; + + /** + * Option to disable redirect to landingPage if there is no saved page to redirect back to. Form Auth POST is followed + * by redirect to landingPage by default. + * + * @deprecated redirect to landingPage can be disabled by removing default landing page + * (via `quarkus.http.auth.form.landing-page=`). Quarkus will ignore this configuration property + * if there is no landing page. + */ + @ConfigItem(defaultValue = "true") + @Deprecated + public boolean redirectAfterLogin; + + /** + * Option to control the name of the cookie used to redirect the user back + * to where he wants to get access to. + */ + @ConfigItem(defaultValue = "quarkus-redirect-location") + public String locationCookie; + + /** + * The inactivity (idle) timeout + * + * When inactivity timeout is reached, cookie is not renewed and a new login is enforced. + */ + @ConfigItem(defaultValue = "PT30M") + public Duration timeout; + + /** + * How old a cookie can get before it will be replaced with a new cookie with an updated timeout, also + * referred to as "renewal-timeout". + * + * Note that smaller values will result in slightly more server load (as new encrypted cookies will be + * generated more often), however larger values affect the inactivity timeout as the timeout is set + * when a cookie is generated. + * + * For example if this is set to 10 minutes, and the inactivity timeout is 30m, if a users last request + * is when the cookie is 9m old then the actual timeout will happen 21m after the last request, as the timeout + * is only refreshed when a new cookie is generated. + * + * In other words no timeout is tracked on the server side; the timestamp is encoded and encrypted in the cookie itself, + * and it is decrypted and parsed with each request. + */ + @ConfigItem(defaultValue = "PT1M") + public Duration newCookieInterval; + + /** + * The cookie that is used to store the persistent session + */ + @ConfigItem(defaultValue = "quarkus-credential") + public String cookieName; + + /** + * The cookie path for the session and location cookies. + */ + @ConfigItem(defaultValue = "/") + public Optional cookiePath = Optional.of("/"); + + /** + * Set the HttpOnly attribute to prevent access to the cookie via JavaScript. + */ + @ConfigItem(defaultValue = "false") + public boolean httpOnlyCookie; + + /** + * SameSite attribute for the session and location cookies. + */ + @ConfigItem(defaultValue = "strict") + public CookieSameSite cookieSameSite = CookieSameSite.STRICT; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java index 62bc6b8ae8ef54..7c16ecd8d00c6c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java @@ -29,6 +29,7 @@ import java.util.Set; import java.util.regex.Pattern; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.jboss.logging.Logger; @@ -41,6 +42,8 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -53,19 +56,6 @@ public class BasicAuthenticationMechanism implements HttpAuthenticationMechanism private static final Logger log = Logger.getLogger(BasicAuthenticationMechanism.class); - public static final String SILENT = "silent"; - public static final String CHARSET = "charset"; - /** - * A comma separated list of patterns and charsets. The pattern is a regular expression. - * - * Because different browsers use different encodings this allows for the correct encoding to be selected based - * on the current browser. In general though it is recommended that BASIC auth not be used when passwords contain - * characters outside ASCII, as some browsers use the current locate to determine encoding. - * - * This list must have an even number of elements, as it is interpreted as pattern,charset,pattern,charset,... - */ - public static final String USER_AGENT_CHARSETS = "user-agent-charsets"; - private final String challenge; private static final String BASIC = "basic"; @@ -85,6 +75,11 @@ public class BasicAuthenticationMechanism implements HttpAuthenticationMechanism private final Charset charset; private final Map userAgentCharsets; + @Inject + BasicAuthenticationMechanism(HttpConfiguration runtimeConfig, HttpBuildTimeConfig buildTimeConfig) { + this(runtimeConfig.auth.realm.orElse(null), buildTimeConfig.auth.form.enabled); + } + public BasicAuthenticationMechanism(final String realmName) { this(realmName, false); } @@ -101,25 +96,6 @@ public BasicAuthenticationMechanism(final String realmName, final boolean silent this.userAgentCharsets = Collections.unmodifiableMap(new LinkedHashMap<>(userAgentCharsets)); } - @Deprecated - public BasicAuthenticationMechanism(final String realmName, final String mechanismName) { - this(realmName, mechanismName, false); - } - - @Deprecated - public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent) { - this(realmName, mechanismName, silent, StandardCharsets.UTF_8, Collections.emptyMap()); - } - - @Deprecated - public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent, - Charset charset, Map userAgentCharsets) { - this.challenge = BASIC_PREFIX + "realm=\"" + realmName + "\""; - this.silent = silent; - this.charset = charset; - this.userAgentCharsets = Collections.unmodifiableMap(new LinkedHashMap<>(userAgentCharsets)); - } - @Override public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index 48bfff2708aadb..639565c632ad2b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -23,6 +23,7 @@ import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; import io.quarkus.vertx.http.runtime.FormAuthConfig; +import io.quarkus.vertx.http.runtime.FormAuthRuntimeConfig; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Uni; @@ -74,22 +75,23 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism key = httpConfiguration.encryptionKey.get(); } FormAuthConfig form = buildTimeConfig.auth.form; - this.loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), - form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), - form.cookiePath.orElse(null)); - this.loginPage = startWithSlash(form.loginPage.orElse(null)); - this.errorPage = startWithSlash(form.errorPage.orElse(null)); - this.landingPage = startWithSlash(form.landingPage.orElse(null)); + FormAuthRuntimeConfig runtimeForm = httpConfiguration.auth.form; + this.loginManager = new PersistentLoginManager(key, runtimeForm.cookieName, runtimeForm.timeout.toMillis(), + runtimeForm.newCookieInterval.toMillis(), runtimeForm.httpOnlyCookie, runtimeForm.cookieSameSite.name(), + runtimeForm.cookiePath.orElse(null)); + this.loginPage = startWithSlash(runtimeForm.loginPage.orElse(null)); + this.errorPage = startWithSlash(runtimeForm.errorPage.orElse(null)); + this.landingPage = startWithSlash(runtimeForm.landingPage.orElse(null)); this.postLocation = startWithSlash(form.postLocation); - this.usernameParameter = form.usernameParameter; - this.passwordParameter = form.passwordParameter; - this.locationCookie = form.locationCookie; - this.cookiePath = form.cookiePath.orElse(null); - boolean redirectAfterLogin = form.redirectAfterLogin; + this.usernameParameter = runtimeForm.usernameParameter; + this.passwordParameter = runtimeForm.passwordParameter; + this.locationCookie = runtimeForm.locationCookie; + this.cookiePath = runtimeForm.cookiePath.orElse(null); + boolean redirectAfterLogin = runtimeForm.redirectAfterLogin; this.redirectToLandingPage = landingPage != null && redirectAfterLogin; this.redirectToLoginPage = loginPage != null; this.redirectToErrorPage = errorPage != null; - this.cookieSameSite = CookieSameSite.valueOf(form.cookieSameSite.name()); + this.cookieSameSite = CookieSameSite.valueOf(runtimeForm.cookieSameSite.name()); } public FormAuthenticationMechanism(String loginPage, String postLocation, diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 40cc75234ddce4..792238ec3f3d6d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -21,7 +21,6 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.MethodDescription; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.CompositeException; import io.smallrye.mutiny.Uni; @@ -55,16 +54,6 @@ public void handle(RoutingContext event) { }; } - public Supplier setupBasicAuth(HttpBuildTimeConfig buildTimeConfig) { - return new Supplier() { - @Override - public BasicAuthenticationMechanism get() { - return new BasicAuthenticationMechanism(buildTimeConfig.auth.realm.orElse(null), - buildTimeConfig.auth.form.enabled); - } - }; - } - /** * This handler resolves the identity, and will be mapped to the post location. Otherwise, * for lazy auth the post will not be evaluated if there is no security rule for the post location. From 785433f920959e8d5ff3aa5de62419014ad5fb48 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 7 Nov 2023 12:07:24 +0000 Subject: [PATCH 46/68] Update SmallRye Config to 3.4.4 --- 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 12ba92c88e127e..7ef21c1ff8fc53 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -51,7 +51,7 @@ 2.0 3.1.1 2.2.0 - 3.4.1 + 3.4.4 4.0.4 4.0.0 3.7.0 From bc14dcf6fc455a7eb70e2c2c3a5e438d2eaa91d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sat, 11 Nov 2023 15:02:15 +0100 Subject: [PATCH 47/68] Document how to log auth failures in RESTEasy Reactive --- docs/src/main/asciidoc/resteasy-reactive-migration.adoc | 6 ++++++ .../main/asciidoc/security-proactive-authentication.adoc | 1 + 2 files changed, 7 insertions(+) diff --git a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc index 86eb5742cf2008..30ef22a2b7993b 100644 --- a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc @@ -155,6 +155,12 @@ public class ReactiveResource { The same is true for your third-party libraries. If they happen to depend on servlets you need to find a migration path for them. +=== Log authentication and authorization failures + +The RESTEasy Reactive endpoint security checks are performed before xref:cdi.adoc#interceptors[CDI interceptors] are invoked. +The safest approach to log Quarkus Security authentication exceptions is to ensure that proactive authentication is enabled and to use Vert.x HTTP route failure handlers. +For more information, see the xref:security-proactive-authentication.adoc#customize-auth-exception-responses[Customize authentication exception responses] section of the Proactive authentication guide. + == Client The Reactive REST Client (`quarkus-rest-client-reactive` and its dependencies) replace the legacy `quarkus-rest-client` but leverage Quarkus' build time processing diff --git a/docs/src/main/asciidoc/security-proactive-authentication.adoc b/docs/src/main/asciidoc/security-proactive-authentication.adoc index 0fbc81236a9020..22f50c364a8e23 100644 --- a/docs/src/main/asciidoc/security-proactive-authentication.adoc +++ b/docs/src/main/asciidoc/security-proactive-authentication.adoc @@ -94,6 +94,7 @@ public class HelloService { } ---- +[[customize-auth-exception-responses]] == Customize authentication exception responses You can use Jakarta REST `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: From c47168c34d84efb6f96feb442e36b6d9cdfdbcf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sat, 11 Nov 2023 23:39:50 +0100 Subject: [PATCH 48/68] Introduce runtime named HTTP Security Policies --- ...ity-authorize-web-endpoints-reference.adoc | 53 +++++++++++- .../HttpSecurityPolicyBuildItem.java | 5 ++ .../deployment/HttpSecurityProcessor.java | 13 ++- .../security/CustomNamedHttpSecPolicy.java | 29 +++++++ .../CustomNamedHttpSecPolicyTest.java | 82 +++++++++++++++++++ ...bstractPathMatchingHttpSecurityPolicy.java | 22 +++-- .../http/runtime/security/HttpAuthorizer.java | 8 +- .../runtime/security/HttpSecurityPolicy.java | 21 +++-- .../security/HttpSecurityRecorder.java | 22 +++-- ...agementPathMatchingHttpSecurityPolicy.java | 7 +- .../PathMatchingHttpSecurityPolicy.java | 18 +--- 11 files changed, 238 insertions(+), 42 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index d503365f9be9b6..41a9d39bf3feda 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -68,6 +68,52 @@ It is an exact path match because it does not end with `*`. <3> This permission set references the previously defined policy. `roles1` is an example name; you can call the permission sets whatever you want. +=== Custom HttpSecurityPolicy + +Sometimes it might be useful to register your own named policy. You can get it done by creating application scoped CDI +bean that implements the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy` interface like in the example below: + +[source,java] +---- +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + if (customRequestAuthorization(request)) { + return Uni.createFrom().item(CheckResult.PERMIT); + } + return Uni.createFrom().item(CheckResult.DENY); + } + + @Override + public String name() { + return "custom"; <1> + } +} +---- +<1> Named HTTP Security policy will only be applied to requests matched by the `application.properties` path matching rules. + +.Example of custom named HttpSecurityPolicy referenced from configuration file +[source,properties] +---- +quarkus.http.auth.permission.custom1.paths=/custom/* +quarkus.http.auth.permission.custom1.policy=custom <1> +---- +<1> Custom policy name must match the value returned by the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method. + +[TIP] +==== +You can also create global `HttpSecurityPolicy` invoked on every request. +Just do not implement the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method and leave the policy nameless. +==== === Matching on paths and methods @@ -642,19 +688,22 @@ Similarly to the `CRUDResource` example, the following example shows how you can ---- package org.acme.library; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.security.Permission; import java.util.Arrays; import java.util.Set; +@RegisterForReflection <1> public class MediaLibraryPermission extends LibraryPermission { public MediaLibraryPermission(String libraryName, String[] actions) { - super(libraryName, actions, new MediaLibrary()); <1> + super(libraryName, actions, new MediaLibrary()); <2> } } ---- -<1> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor. +<1> When building a native executable, the permission class must be registered for reflection unless it is also used in at least one `io.quarkus.security.PermissionsAllowed#name` parameter. +<2> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor. [source,properties] ---- diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java index cfa973812af249..eea7841fecdb92 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java @@ -5,6 +5,11 @@ import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +/** + * @deprecated Define {@link io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy} CDI bean with {@link #name} + * set as the {@link HttpSecurityPolicy#name()}. + */ +@Deprecated public final class HttpSecurityPolicyBuildItem extends MultiBuildItem { final String name; diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index fb34fd418ef281..f248bb3428b5e9 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -33,6 +33,7 @@ import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder; import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy; @@ -45,10 +46,18 @@ public class HttpSecurityProcessor { @Record(ExecutionTime.STATIC_INIT) @BuildStep void produceNamedHttpSecurityPolicies(List httpSecurityPolicyBuildItems, + BuildProducer syntheticBeanProducer, HttpSecurityRecorder recorder) { if (!httpSecurityPolicyBuildItems.isEmpty()) { - recorder.setBuildTimeNamedPolicies(httpSecurityPolicyBuildItems.stream().collect( - Collectors.toMap(HttpSecurityPolicyBuildItem::getName, HttpSecurityPolicyBuildItem::getPolicySupplier))); + httpSecurityPolicyBuildItems.forEach(item -> syntheticBeanProducer + .produce(SyntheticBeanBuildItem + .configure(HttpSecurityPolicy.class) + .named(HttpSecurityPolicy.class.getName() + "." + item.getName()) + .runtimeValue(recorder.createNamedHttpSecurityPolicy(item.getPolicySupplier(), item.getName())) + .addType(HttpSecurityPolicy.class) + .scope(Singleton.class) + .unremovable() + .done())); } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java new file mode 100644 index 00000000000000..61d8213fd9e2ba --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java @@ -0,0 +1,29 @@ +package io.quarkus.vertx.http.security; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + if (isRequestAuthorized(request)) { + return Uni.createFrom().item(CheckResult.PERMIT); + } + return Uni.createFrom().item(CheckResult.DENY); + } + + private static boolean isRequestAuthorized(RoutingContext request) { + return request.request().headers().contains("hush-hush"); + } + + @Override + public String name() { + return "custom123"; + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java new file mode 100644 index 00000000000000..5e5aae1dfb2df1 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http.security; + +import java.util.function.Supplier; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class CustomNamedHttpSecPolicyTest { + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.authenticated.paths=admin\n" + + "quarkus.http.auth.permission.authenticated.policy=custom123\n"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityController.class, TestIdentityProvider.class, AdminPathHandler.class, + CustomNamedHttpSecPolicy.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties"); + } + }); + + @Test + public void testAdminPath() { + RestAssured + .given() + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(401); + RestAssured + .given() + .when() + .header("hush-hush", "ignored") + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(Matchers.equalTo(":/admin")); + RestAssured + .given() + .auth() + .preemptive() + .basic("test", "test") + .when() + .header("hush-hush", "ignored") + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(Matchers.equalTo("test:/admin")); + RestAssured + .given() + .auth() + .preemptive() + .basic("test", "test") + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(403); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java index fa82007a034a3a..304688d56f51d8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java @@ -14,6 +14,8 @@ import java.util.Set; import java.util.function.Function; +import jakarta.enterprise.inject.Instance; + import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; @@ -34,8 +36,8 @@ public class AbstractPathMatchingHttpSecurityPolicy { private final PathMatcher> pathMatcher = new PathMatcher<>(); AbstractPathMatchingHttpSecurityPolicy(Map permissions, - Map rolePolicy, String rootPath, Map namedBuildTimePolicies) { - init(permissions, toNamedHttpSecPolicies(rolePolicy, namedBuildTimePolicies), rootPath); + Map rolePolicy, String rootPath, Instance installedPolicies) { + init(permissions, toNamedHttpSecPolicies(rolePolicy, installedPolicies), rootPath); } public String getAuthMechanismName(RoutingContext routingContext) { @@ -158,11 +160,21 @@ public List findPermissionCheckers(RoutingContext context) { } private static Map toNamedHttpSecPolicies(Map rolePolicies, - Map namedBuildTimePolicies) { + Instance installedPolicies) { Map namedPolicies = new HashMap<>(); - if (!namedBuildTimePolicies.isEmpty()) { - namedPolicies.putAll(namedBuildTimePolicies); + for (Instance.Handle handle : installedPolicies.handles()) { + if (handle.getBean().getBeanClass().getSuperclass() == AbstractPathMatchingHttpSecurityPolicy.class) { + continue; + } + var policy = handle.get(); + if (policy.name() != null) { + if (policy.name().isBlank()) { + throw new ConfigurationException("HTTP Security policy '" + policy + "' name must not be blank"); + } + namedPolicies.put(policy.name(), policy); + } } + for (Map.Entry e : rolePolicies.entrySet()) { PolicyConfig policyConfig = e.getValue(); if (policyConfig.permissions.isEmpty()) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java index 190a7774888738..6f4529041ab3b4 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java @@ -23,10 +23,12 @@ public class HttpAuthorizer extends AbstractHttpAuthorizer { } private static List toList(Instance installedPolicies) { - List policies = new ArrayList<>(); + List globalPolicies = new ArrayList<>(); for (HttpSecurityPolicy i : installedPolicies) { - policies.add(i); + if (i.name() == null) { + globalPolicies.add(i); + } } - return policies; + return globalPolicies; } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java index 88a10a5da19324..f0c9062360111e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java @@ -8,18 +8,27 @@ /** * An HTTP Security policy, that controls which requests are allowed to proceed. - * - * There are two different ways these policies can be installed. The easiest is to just create a CDI bean, in which - * case the policy will be invoked on every request. - * - * Alternatively HttpSecurityPolicyBuildItem can be used to create a named policy. This policy can then be referenced - * in the application.properties path matching rules, which allows this policy to be applied to specific requests. + * CDI beans implementing this interface are invoked on every request unless they define {@link #name()}. + * The policy with {@link #name()} can then be referenced in the application.properties path matching rules, + * which allows this policy to be applied only to specific requests. */ public interface HttpSecurityPolicy { Uni checkPermission(RoutingContext request, Uni identity, AuthorizationRequestContext requestContext); + /** + * HTTP Security policy name referenced in the application.properties path matching rules, which allows this + * policy to be applied to specific requests. The name must not be blank. When the name is {@code null}, policy + * will be applied to every request. + * + * @return policy name + */ + default String name() { + // null == global policy + return null; + } + /** * The results of a permission check */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 40cc75234ddce4..fe3b4ede94414b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -110,12 +110,22 @@ public EagerSecurityInterceptorStorage get() { }; } - public void setBuildTimeNamedPolicies(Map> buildTimeNamedPolicies) { - Map nameToPolicy = new HashMap<>(); - for (Map.Entry> nameToSupplier : buildTimeNamedPolicies.entrySet()) { - nameToPolicy.put(nameToSupplier.getKey(), nameToSupplier.getValue().get()); - } - PathMatchingHttpSecurityPolicy.replaceNamedBuildTimePolicies(nameToPolicy); + public RuntimeValue createNamedHttpSecurityPolicy(Supplier policySupplier, + String name) { + return new RuntimeValue<>(new HttpSecurityPolicy() { + private final HttpSecurityPolicy delegate = policySupplier.get(); + + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + return delegate.checkPermission(request, identity, requestContext); + } + + @Override + public String name() { + return name; + } + }); } public static abstract class DefaultAuthFailureHandler implements BiConsumer { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java index 4a22ac63fa04e1..037c3ceed32bb7 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java @@ -1,7 +1,6 @@ package io.quarkus.vertx.http.runtime.security; -import java.util.Map; - +import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; import io.quarkus.runtime.Startup; @@ -18,8 +17,8 @@ public class ManagementPathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy { ManagementPathMatchingHttpSecurityPolicy(ManagementInterfaceBuildTimeConfig buildTimeConfig, - ManagementInterfaceConfiguration runTimeConfig) { - super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, Map.of()); + ManagementInterfaceConfiguration runTimeConfig, Instance installedPolicies) { + super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java index c5624c4b6c7a4b..b258e4fa0be0ee 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java @@ -1,8 +1,6 @@ package io.quarkus.vertx.http.runtime.security; -import java.util.HashMap; -import java.util.Map; - +import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; import io.quarkus.runtime.Startup; @@ -18,17 +16,9 @@ @Singleton public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy implements HttpSecurityPolicy { - // this map is planned for removal very soon as runtime named policies will make it obsolete - private static final Map HTTP_SECURITY_BUILD_TIME_POLICIES = new HashMap<>(); - - PathMatchingHttpSecurityPolicy(HttpConfiguration httpConfig, HttpBuildTimeConfig buildTimeConfig) { - super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, - HTTP_SECURITY_BUILD_TIME_POLICIES); - } - - static synchronized void replaceNamedBuildTimePolicies(Map newPolicies) { - HTTP_SECURITY_BUILD_TIME_POLICIES.clear(); - HTTP_SECURITY_BUILD_TIME_POLICIES.putAll(newPolicies); + PathMatchingHttpSecurityPolicy(HttpConfiguration httpConfig, HttpBuildTimeConfig buildTimeConfig, + Instance installedPolicies) { + super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies); } } From 966ddba313b666ac155f5e9f85ed2d5f3777b52f Mon Sep 17 00:00:00 2001 From: kdnakt Date: Sun, 12 Nov 2023 08:26:55 +0900 Subject: [PATCH 49/68] Fix typos in reactive-sql-clients.adoc --- docs/src/main/asciidoc/reactive-sql-clients.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 27d5101aecb606..4bc2423e03e522 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -207,7 +207,7 @@ client.query("DROP TABLE IF EXISTS fruits").execute() .await().indefinitely(); ---- -NOTE: Wondering why we need block until the latest query is completed? +NOTE: Wondering why we need to block until the latest query is completed? This code is part of a `@PostConstruct` method and Quarkus invokes it synchronously. As a consequence, returning prematurely could lead to serving requests while the database is not ready yet. @@ -413,7 +413,7 @@ We will use https://jquery.com/[jQuery] to simplify interactions with the backen ---- -In the Javascript code, we need a function to refresh the list of fruits when: +In the JavaScript code, we need a function to refresh the list of fruits when: * the page is loaded, or * a fruit is added, or @@ -814,7 +814,7 @@ public class PipeliningExample { PgPool client; public Uni favoriteFruitAndVegetable() { - // Explicitely acquire a connection + // Explicitly acquire a connection return client.withConnection(conn -> { Uni favoriteFruit = conn.query("SELECT name FROM fruits WHERE preferred IS TRUE").execute() .onItem().transform(rows -> rows.iterator().next().getString("name")); From a94eb39233bccf71fca18aec8285665cf0072786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sun, 12 Nov 2023 21:05:46 +0100 Subject: [PATCH 50/68] Fix HttpSecurityProcessor compilation by importing Singleton --- .../io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index b349845f3a228d..65ed375ee01cd4 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -12,6 +12,7 @@ import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Singleton; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; From a8d766d27cf15fa2e587a120b82ad14554744efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Mon, 13 Nov 2023 08:57:12 +0100 Subject: [PATCH 51/68] Adjust mysql-connector-j native substitutions following dependency upgrade --- .../graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java index faa9f278d190a2..ced7df3594206b 100644 --- a/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java +++ b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java @@ -9,6 +9,14 @@ @TargetClass(className = "com.mysql.cj.protocol.a.authentication.AuthenticationOciClient") final class AuthenticationOciClient { + @Substitute + private void loadOciConfig() { + throw ExceptionFactory + .createException("OciClient authentication is not available in Quarkus when compiling to native-image:" + + " the MySQL JDBC driver team needs to cleanup the dependency requirements to make this possible." + + " If you need this resolved, please open a support request."); + } + @Substitute private void initializePrivateKey() { throw ExceptionFactory From 1ae38463464dc5963d20dd88ff1ac271785c12ea Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Sun, 12 Nov 2023 21:05:41 +0100 Subject: [PATCH 52/68] [#35835] Update artifact ids for dependencies in Blaze-Persistence documentation for Quarkus 3 integration --- docs/src/main/asciidoc/blaze-persistence.adoc | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/blaze-persistence.adoc b/docs/src/main/asciidoc/blaze-persistence.adoc index 4defc700a386be..e2ad4aca60fb12 100644 --- a/docs/src/main/asciidoc/blaze-persistence.adoc +++ b/docs/src/main/asciidoc/blaze-persistence.adoc @@ -33,10 +33,13 @@ In Quarkus, you just need to: Add the following dependencies to your project: -* the Blaze-Persistence extension: `com.blazebit:blaze-persistence-integration-quarkus` +* the Blaze-Persistence extension: `com.blazebit:blaze-persistence-integration-quarkus-3` * further Blaze-Persistence integrations as needed: - - `blaze-persistence-integration-jackson` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#Jackson%20integration[Jackson] + - `blaze-persistence-integration-jackson-jakarta` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#Jackson%20integration[Jackson] + - `blaze-persistence-integration-jsonb-jakarta` for link:https://persistence.blazebit.com/documentation/1.6/entity-view/manual/en_US/#jsonb-integration[JSONB] - `blaze-persistence-integration-jaxrs` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#jaxrs-integration[Jakarta REST] + - `blaze-persistence-integration-jaxrs-jackson-jakarta` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#jaxrs-integration[Jakarta REST with Jackson] + - `blaze-persistence-integration-jaxrs-jsonb-jakarta` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#jaxrs-integration[Jakarta REST with JSONB] [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .Example dependencies using Maven @@ -44,11 +47,11 @@ Add the following dependencies to your project: com.blazebit - blaze-persistence-integration-quarkus + blaze-persistence-integration-quarkus-3 com.blazebit - blaze-persistence-integration-hibernate-5.6 + blaze-persistence-integration-hibernate-6.2 runtime ---- @@ -56,8 +59,8 @@ Add the following dependencies to your project: [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .Using Gradle ---- -implementation("com.blazebit:blaze-persistence-integration-quarkus") -runtimeOnly("com.blazebit:blaze-persistence-integration-hibernate-5.6") +implementation("com.blazebit:blaze-persistence-integration-quarkus-3") +runtimeOnly("com.blazebit:blaze-persistence-integration-hibernate-6.2") ---- The use in native images requires a dependency on the entity view annotation processor that may be extracted into a separate `native` profile: @@ -70,7 +73,7 @@ The use in native images requires a dependency on the entity view annotation pro com.blazebit - blaze-persistence-entity-view-processor + blaze-persistence-entity-view-processor-jakarta provided From f599aa6b67756449536570945853de588d85bca4 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 13 Nov 2023 11:05:50 +0100 Subject: [PATCH 53/68] Disable CustomManifestArgumentsTest on Windows It has been failing consistently for days now. Things are working fine on Linux. --- .../java/io/quarkus/gradle/CustomManifestArgumentsTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/CustomManifestArgumentsTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/CustomManifestArgumentsTest.java index d7aafae527d76a..7b7ae3c97e2141 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/CustomManifestArgumentsTest.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/CustomManifestArgumentsTest.java @@ -11,10 +11,13 @@ import java.util.jar.Manifest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; public class CustomManifestArgumentsTest extends QuarkusGradleWrapperTestBase { @Test + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "for whatever reason, this is not working anymore on Widndows, the customSection is null...") public void shouldContainsSpecificManifestProperty() throws Exception { File projectDir = getProjectDir("custom-config-java-module"); From 26713dbd5c26bdc8e723bbcafe81228c50637c8b Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Fri, 10 Nov 2023 15:34:05 +0100 Subject: [PATCH 54/68] test(kubernetes-client): allow incomplete model classpath Prevent Quarkus from adding --link-at-build-time GraalVM flag Signed-off-by: Marc Nuri --- .../deployment/pom.xml | 58 +++++++++++++++++++ .../client/deployment/NativeOverrides.java | 12 ++++ .../kubernetes-client-hack-extension/pom.xml | 21 +++++++ .../runtime/pom.xml | 48 +++++++++++++++ .../resources/META-INF/quarkus-extension.yaml | 11 ++++ integration-tests/kubernetes-client/pom.xml | 18 ++++++ integration-tests/openshift-client/pom.xml | 22 +++++++ integration-tests/pom.xml | 1 + 8 files changed, 191 insertions(+) create mode 100644 integration-tests/kubernetes-client-hack-extension/deployment/pom.xml create mode 100644 integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java create mode 100644 integration-tests/kubernetes-client-hack-extension/pom.xml create mode 100644 integration-tests/kubernetes-client-hack-extension/runtime/pom.xml create mode 100644 integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml b/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml new file mode 100644 index 00000000000000..913462707b124c --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + io.quarkus + quarkus-integration-test-kubernetes-client-hack-extension-parent + 999-SNAPSHOT + + + quarkus-integration-test-kubernetes-client-hack-extension-deployment + Quarkus - Integration Tests - Kubernetes Client Hack Extension - Deployment + + + + io.quarkus + quarkus-core-deployment + ${project.version} + + + io.quarkus + quarkus-integration-test-kubernetes-client-hack-extension + ${project.version} + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + org.codehaus.mojo + templating-maven-plugin + 1.0.0 + + + filtering-java-templates + + filter-sources + + + + + + + diff --git a/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java b/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java new file mode 100644 index 00000000000000..91372ea46589e1 --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java @@ -0,0 +1,12 @@ +package io.quarkus.kubernetes.client.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathBuildItem; + +public class NativeOverrides { + + @BuildStep + NativeImageAllowIncompleteClasspathBuildItem incompleteModel() { + return new NativeImageAllowIncompleteClasspathBuildItem("quarkus-kubernetes-client"); + } +} diff --git a/integration-tests/kubernetes-client-hack-extension/pom.xml b/integration-tests/kubernetes-client-hack-extension/pom.xml new file mode 100644 index 00000000000000..c29e01f59239c3 --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../../extensions/pom.xml + + + quarkus-integration-test-kubernetes-client-hack-extension-parent + Quarkus - Integration Tests - Kubernetes Client Hack Extension + pom + + deployment + runtime + + diff --git a/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml b/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml new file mode 100644 index 00000000000000..a0278c43dafaa9 --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + io.quarkus + quarkus-integration-test-kubernetes-client-hack-extension-parent + 999-SNAPSHOT + + + quarkus-integration-test-kubernetes-client-hack-extension + Quarkus - Integration Tests - Kubernetes Client Hack Extension - Runtime + + + + io.quarkus + quarkus-core + ${project.version} + + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${project.version} + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..42eda1b64d7c59 --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Kubernetes Client Hack Extension" +metadata: + keywords: + - "kubernetes-client" + guide: "https://quarkus.io/guides/kubernetes-client" + categories: + - "cloud" + status: "test" + config: diff --git a/integration-tests/kubernetes-client/pom.xml b/integration-tests/kubernetes-client/pom.xml index e42dbf99001624..a08bd4306d3977 100644 --- a/integration-tests/kubernetes-client/pom.xml +++ b/integration-tests/kubernetes-client/pom.xml @@ -22,6 +22,11 @@ io.quarkus quarkus-kubernetes-config + + io.quarkus + quarkus-integration-test-kubernetes-client-hack-extension + ${project.version} + io.quarkus quarkus-openshift-client @@ -85,6 +90,19 @@ + + io.quarkus + quarkus-integration-test-kubernetes-client-hack-extension-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-openshift-client-deployment diff --git a/integration-tests/openshift-client/pom.xml b/integration-tests/openshift-client/pom.xml index faf03cd12a8341..eef764d55f5e64 100644 --- a/integration-tests/openshift-client/pom.xml +++ b/integration-tests/openshift-client/pom.xml @@ -13,11 +13,20 @@ quarkus-integration-test-openshift-client Quarkus - Integration Tests - OpenShift Client + + true + + io.quarkus quarkus-resteasy-jackson + + io.quarkus + quarkus-integration-test-kubernetes-client-hack-extension + ${project.version} + io.quarkus quarkus-openshift-client @@ -61,6 +70,19 @@ + + io.quarkus + quarkus-integration-test-kubernetes-client-hack-extension-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-openshift-client-deployment diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index b623b32388ee67..58ff872da1abd9 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -285,6 +285,7 @@ amazon-lambda-http-resteasy amazon-lambda-http-resteasy-reactive container-image + kubernetes-client-hack-extension kubernetes kubernetes-client kubernetes-client-devservices From 13b73a96370623a95de2c19f637d332820a72381 Mon Sep 17 00:00:00 2001 From: brunobat Date: Wed, 8 Nov 2023 10:33:21 +0000 Subject: [PATCH 55/68] Fix OTel Resource Attributes --- .../InvalidAttributesConfigTest.java | 31 ++++++++ .../runtime/OpenTelemetryRecorder.java | 79 +++++++++++-------- 2 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/InvalidAttributesConfigTest.java diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/InvalidAttributesConfigTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/InvalidAttributesConfigTest.java new file mode 100644 index 00000000000000..f2d1f661158ec6 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/InvalidAttributesConfigTest.java @@ -0,0 +1,31 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.NoSuchElementException; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class InvalidAttributesConfigTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .overrideConfigKey("quarkus.otel.resource.attributes", "pod.id=${SOMETHING}") + .assertException(new Consumer() { + @Override + public void accept(final Throwable throwable) { + throwable.getCause().printStackTrace(); + assertTrue(throwable.getCause() instanceof NoSuchElementException); + assertEquals("SRCFG00011: Could not expand value SOMETHING in property quarkus.otel.resource.attributes", + throwable.getCause().getMessage()); + } + }); + + @Test + void test() { + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java index 9c57af1d76d2f1..547a40b10789d5 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java @@ -10,6 +10,7 @@ import jakarta.enterprise.util.TypeLiteral; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.Converter; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; @@ -20,8 +21,6 @@ import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.DurationConverter; -import io.smallrye.config.ConfigValue; -import io.smallrye.config.NameIterator; import io.smallrye.config.SmallRyeConfig; import io.vertx.core.Vertx; @@ -89,46 +88,58 @@ private Map getOtelConfigs() { // load new properties for (String propertyName : config.getPropertyNames()) { if (propertyName.startsWith("quarkus.otel.")) { - ConfigValue configValue = config.getConfigValue(propertyName); - if (configValue.getValue() != null) { - NameIterator name = new NameIterator(propertyName); - name.next(); - oTelConfigs.put(name.getName().substring(name.getPosition() + 1), getValue(configValue)); - } + String value = getValue(config, propertyName); + oTelConfigs.put(propertyName.substring(8), value); } } return oTelConfigs; } - /** - * Transforms the value to what OTel expects - * TODO: this is super simplistic, and should be more modular if needed - */ - private String getValue(ConfigValue configValue) { - String name = configValue.getName(); - if (name.endsWith("timeout") || name.endsWith("delay")) { - String value = configValue.getValue(); - if (DurationConverter.DIGITS.asPredicate().test(value)) { - // OTel regards values without a unit to me milliseconds instead of seconds - // that java.time.Duration assumes, so let's just not do any conversion and let OTel handle with it - return value; - } - Duration duration; - try { - duration = DurationConverter.parseDuration(value); - } catch (Exception ignored) { - // it's not a Duration, so we can't do much - return configValue.getValue(); - } - try { - return duration.toMillis() + "ms"; - } catch (Exception ignored) { - return duration.toSeconds() + "s"; - } + private String getValue(SmallRyeConfig config, String propertyName) { + if (propertyName.endsWith("timeout") || propertyName.endsWith("delay")) { + return config.getValue(propertyName, OTelDurationConverter.INSTANCE); + } else { + return config.getValue(propertyName, String.class); } - return configValue.getValue(); } }; } + /** + * Transforms the value to what OTel expects + * TODO: this is super simplistic, and should be more modular if needed + */ + private static class OTelDurationConverter implements Converter { + static OTelDurationConverter INSTANCE = new OTelDurationConverter(); + + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + if (value == null) { + throw new NullPointerException(); + } + + if (DurationConverter.DIGITS.asPredicate().test(value)) { + // OTel regards values without a unit to me milliseconds instead of seconds + // that java.time.Duration assumes, so let's just not do any conversion and let OTel handle with it + return value; + } + Duration duration; + try { + duration = DurationConverter.parseDuration(value); + } catch (Exception ignored) { + // it's not a Duration, so we can't do much + return value; + } + + if (duration == null) { + return value; + } + + try { + return duration.toMillis() + "ms"; + } catch (Exception ignored) { + return duration.toSeconds() + "s"; + } + } + } } From 042aed288e3f27db4faa7a9ad0e29af817c0c47b Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 13 Nov 2023 11:51:50 +0200 Subject: [PATCH 56/68] Use empty string in Sse event when there is no data Using an empty string instead of null is what the classic rest client does, so let's align with it Closes: #37033 --- .../reactive/jsonb/deployment/test/sse/SseParserTest.java | 8 ++++---- .../reactive/server/test/stream/StreamTestCase.java | 2 +- .../jboss/resteasy/reactive/client/impl/SseParser.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java index 7f8bd584a02447..36cca2e23779e2 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java @@ -37,14 +37,14 @@ public void testParser() { testParser("data:foo\ndata:\ndata:bar\n\n", "foo\n\nbar", null, null, null, SseEvent.RECONNECT_NOT_SET); // no data: no event - testParser("\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); - testParser("data:\n\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); - testParser("data\n\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("data:\n\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("data\n\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); // all fields testParser("data:DATA\nid:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", "DATA", "COMMENT", "ID", "NAME", 23); // all fields and no data - testParser("id:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", null, "COMMENT", "ID", "NAME", 23); + testParser("id:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", "", "COMMENT", "ID", "NAME", 23); // optional space after colon testParser("data:foo\n\n", "foo", null, null, null, SseEvent.RECONNECT_NOT_SET); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java index 7a2135bd216cd0..9da6a2fa1a778a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java @@ -264,7 +264,7 @@ public void testSseForMultiWithOutboundSseEvent() throws InterruptedException { }); sse.open(); Assertions.assertTrue(latch.await(20, TimeUnit.SECONDS)); - org.assertj.core.api.Assertions.assertThat(results).containsExactly(null, "uno", "dos", "tres"); + org.assertj.core.api.Assertions.assertThat(results).containsExactly("", "uno", "dos", "tres"); org.assertj.core.api.Assertions.assertThat(ids).containsExactly(null, "one", "two", "three"); org.assertj.core.api.Assertions.assertThat(names).containsExactly(null, "eins", "zwei", "drei"); org.assertj.core.api.Assertions.assertThat(comments).containsExactly("dummy", null, null, null); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java index 08525139dc0e98..46bb82858514ab 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java @@ -158,7 +158,7 @@ private void dispatchEvent() { event.setComment(commentBuffer.length() == 0 ? null : commentBuffer.toString()); // SSE spec says empty string is the default, but JAX-RS says null if not specified event.setId(lastEventId); - event.setData(dataBuffer.length() == 0 ? null : dataBuffer.toString()); + event.setData(dataBuffer.length() == 0 ? "" : dataBuffer.toString()); // SSE spec says "message" is the default, but JAX-RS says null if not specified event.setName(eventType); event.setReconnectDelay(eventReconnectTime); From bfdc329e23db8289a93e3d2b73c7503cb4134f77 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Fri, 10 Nov 2023 21:19:35 +0100 Subject: [PATCH 57/68] Set all discovered quarkus.* properties as system properties in QuarkusWorker --- .../io/quarkus/gradle/tasks/QuarkusTask.java | 5 +- .../application/build.gradle | 32 ++++++++++++ .../application/gradle.properties | 2 + .../application/settings.gradle | 20 +++++++ .../java/org/acme/ExampleResourceIT.java | 29 +++++++++++ .../acme/quarkus/sample/HelloResource.java | 33 ++++++++++++ .../src/main/resources/application.properties | 1 + .../extensions/example-extension/build.gradle | 34 ++++++++++++ .../example-extension/deployment/build.gradle | 18 +++++++ .../extension/deployment/ExampleConfig.java | 14 +++++ .../deployment/ExampleProcessor.java | 30 +++++++++++ .../example-extension/gradle.properties | 2 + .../example-extension/runtime/build.gradle | 20 +++++++ .../runtime/ExampleBuildOptions.java | 10 ++++ .../extension/runtime/ExampleRecorder.java | 13 +++++ .../runtime/ExampleRuntimeConfig.java | 16 ++++++ .../META-INF/quarkus-extension.properties | 1 + .../resources/META-INF/quarkus-extension.yaml | 12 +++++ .../example-extension/settings.gradle | 21 ++++++++ .../gradle.properties | 2 + .../settings.gradle | 18 +++++++ ...ystemPropsAsBuildTimeConfigSourceTest.java | 52 +++++++++++++++++++ .../nativeimage/BasicJavaNativeBuildIT.java | 6 +-- 23 files changed, 385 insertions(+), 6 deletions(-) create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/build.gradle create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/gradle.properties create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/settings.gradle create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/integrationTest/java/org/acme/ExampleResourceIT.java create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/java/org/acme/quarkus/sample/HelloResource.java create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/resources/application.properties create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/build.gradle create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/build.gradle create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleConfig.java create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleProcessor.java create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/gradle.properties create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/build.gradle create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleBuildOptions.java create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRecorder.java create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRuntimeConfig.java create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.properties create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/settings.gradle create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/gradle.properties create mode 100644 integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/settings.gradle create mode 100644 integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java index 09d16509a05e52..3294472909fb14 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java @@ -20,8 +20,7 @@ import io.quarkus.utilities.OS; public abstract class QuarkusTask extends DefaultTask { - private static final List WORKER_BUILD_FORK_OPTIONS = List.of("quarkus.package.", - "quarkus.application.", "quarkus.gradle-worker.", "quarkus.analytics."); + private static final List WORKER_BUILD_FORK_OPTIONS = List.of("quarkus."); private final transient QuarkusPluginExtension extension; protected final File projectDir; @@ -89,7 +88,7 @@ private void configureProcessWorkerSpec(ProcessWorkerSpec processWorkerSpec, Map } // It's kind of a "very big hammer" here, but this way we ensure that all necessary properties - // ("quarkus.package.*","quarkus.application,*", "quarkus.gradle-worker.*") from all configuration sources + // "quarkus.*" from all configuration sources // are (forcefully) used in the Quarkus build - even properties defined on the QuarkusPluginExtension. // This prevents that settings from e.g. a application.properties takes precedence over an explicit // setting in Gradle project properties, the Quarkus extension or even via the environment or system diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/build.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/build.gradle new file mode 100644 index 00000000000000..556e19daf29830 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/build.gradle @@ -0,0 +1,32 @@ +plugins{ + id "java" + id "io.quarkus" +} + + + +group 'io.quarkus.test.application' +version '1.0-SNAPSHOT' + + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy-reactive' + implementation ('org.acme.extensions:example-extension') + + testImplementation 'io.quarkus:quarkus-junit5' + testImplementation 'io.rest-assured:rest-assured' +} + +test { + useJUnitPlatform() +} + +quarkusIntTest { + environment "MY_RT_NAME", "genadiy" +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/gradle.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/settings.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/settings.gradle new file mode 100644 index 00000000000000..7eeaae22f27fc8 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/settings.gradle @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + } + } + mavenCentral() + gradlePluginPortal() + } + //noinspection GroovyAssignabilityCheck + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} + +includeBuild('../extensions/example-extension'){ + +} +rootProject.name='application' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/integrationTest/java/org/acme/ExampleResourceIT.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/integrationTest/java/org/acme/ExampleResourceIT.java new file mode 100644 index 00000000000000..6219887bad7f46 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/integrationTest/java/org/acme/ExampleResourceIT.java @@ -0,0 +1,29 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusIntegrationTest +public class ExampleResourceIT { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(is("hello cheburashka")); + } + + @Test + public void testRuntimeName() { + given() + .when().get("/hello/runtime-name") + .then() + .statusCode(200) + .body(is("genadiy")); + } +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/java/org/acme/quarkus/sample/HelloResource.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/java/org/acme/quarkus/sample/HelloResource.java new file mode 100644 index 00000000000000..50a4aced5c534b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/java/org/acme/quarkus/sample/HelloResource.java @@ -0,0 +1,33 @@ +package org.acme.quarkus.sample; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.acme.example.extension.runtime.ExampleBuildOptions; +import org.acme.example.extension.runtime.ExampleRuntimeConfig; + +@Path("/hello") +public class HelloResource { + + @Inject + ExampleBuildOptions buildOptions; + + @Inject + ExampleRuntimeConfig rtConfig; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "hello " + buildOptions.name; + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/runtime-name") + public String runtimeName() { + return rtConfig.runtimeName; + } +} diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/resources/application.properties new file mode 100644 index 00000000000000..39fdee9f38d1e7 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.example.runtime-name=${MY_RT_NAME:none} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/build.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/build.gradle new file mode 100644 index 00000000000000..464a421fce2a9a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/build.gradle @@ -0,0 +1,34 @@ +plugins{ + id 'java-library' + id 'maven-publish' +} +subprojects {subProject-> + apply plugin: 'java-library' + apply plugin: 'maven-publish' + + group 'org.acme.extensions' + version '1.0-SNAPSHOT' + publishing { + publications { + maven(MavenPublication) { + groupId = 'org.acme.extensions' + artifactId = subProject.name + version = '1.0-SNAPSHOT' + from components.java + } + } + } +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'org.acme.extensions' + artifactId = rootProject.name + version = '1.0-SNAPSHOT' + from components.java + } + } +} +group 'org.acme.extensions' +version '1.0-SNAPSHOT' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/build.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/build.gradle new file mode 100644 index 00000000000000..3ce19bef01ea11 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'java-library' +} +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + annotationProcessor "io.quarkus:quarkus-extension-processor:${quarkusPlatformVersion}" + + + api project(':example-extension') + implementation 'io.quarkus:quarkus-arc-deployment' +} + diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleConfig.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleConfig.java new file mode 100644 index 00000000000000..d92ed03d4757e1 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleConfig.java @@ -0,0 +1,14 @@ +package org.acme.example.extension.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot +public class ExampleConfig { + + /** + * name + */ + @ConfigItem(defaultValue = "none") + String name; +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleProcessor.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleProcessor.java new file mode 100644 index 00000000000000..7368451542ce07 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleProcessor.java @@ -0,0 +1,30 @@ +package org.acme.example.extension.deployment; + +import jakarta.inject.Singleton; + +import org.acme.example.extension.runtime.ExampleBuildOptions; +import org.acme.example.extension.runtime.ExampleRecorder; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class ExampleProcessor { + + private static final String FEATURE = "example"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + SyntheticBeanBuildItem syntheticBean(ExampleRecorder recorder, ExampleConfig config) { + return SyntheticBeanBuildItem.configure(ExampleBuildOptions.class) + .scope(Singleton.class) + .runtimeValue(recorder.buildOptions(config.name)) + .done(); + } +} diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/gradle.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/build.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/build.gradle new file mode 100644 index 00000000000000..29ba93c67b5d6e --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/build.gradle @@ -0,0 +1,20 @@ + +plugins { + id 'io.quarkus.extension' +} + +quarkusExtension { + deploymentModule = 'example-extension-deployment' +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + annotationProcessor "io.quarkus:quarkus-extension-processor:${quarkusPlatformVersion}" + implementation 'io.quarkus:quarkus-arc' +} + diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleBuildOptions.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleBuildOptions.java new file mode 100644 index 00000000000000..79c18d4929b67d --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleBuildOptions.java @@ -0,0 +1,10 @@ +package org.acme.example.extension.runtime; + +public class ExampleBuildOptions { + + public final String name; + + public ExampleBuildOptions(String name) { + this.name = name; + } +} diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRecorder.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRecorder.java new file mode 100644 index 00000000000000..3c41ac8f05ddc9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRecorder.java @@ -0,0 +1,13 @@ +package org.acme.example.extension.runtime; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class ExampleRecorder { + + public RuntimeValue buildOptions(String name) { + System.out.println("ExampleRecorder.buildOptions " + name); + return new RuntimeValue<>(new ExampleBuildOptions(name)); + } +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRuntimeConfig.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRuntimeConfig.java new file mode 100644 index 00000000000000..8e4890e928ce54 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRuntimeConfig.java @@ -0,0 +1,16 @@ +package org.acme.example.extension.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public class ExampleRuntimeConfig { + + /** + * Whether the banner will be displayed + */ + @ConfigItem(defaultValue = "none") + public String runtimeName; + +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.properties new file mode 100644 index 00000000000000..2e1a6326847e11 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.properties @@ -0,0 +1 @@ +deployment-artifact=org.acme.extensions\:example-extension-deployment\:1.0 \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..12a5c710c9e823 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,12 @@ +--- +name: Quarkus Example Extension +artifact: ${project.groupId}:${project.artifactId}:${project.version} +metadata: + config: + - "quarkus.example.extension." + keywords: + - "logzio" + - "logging" + categories: + - "logging" +description: "Quarkus example extension" \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/settings.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/settings.gradle new file mode 100644 index 00000000000000..ca835fae5d2dbb --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/settings.gradle @@ -0,0 +1,21 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenLocal() + } + plugins { + id 'io.quarkus.extension' version "${quarkusPluginVersion}" + } +} +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + } + +} +rootProject.name = 'example-extension-parent' +include(':deployment') +include(':runtime') +project(':deployment').name='example-extension-deployment' +project(':runtime').name='example-extension' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/gradle.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/gradle.properties new file mode 100644 index 00000000000000..8f063b7d88ba4a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/settings.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/settings.gradle new file mode 100644 index 00000000000000..53eb6f0795e295 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + } + } + mavenCentral() + gradlePluginPortal() + } + //noinspection GroovyAssignabilityCheck + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} + +includeBuild('extensions/example-extension') +includeBuild('application') \ No newline at end of file diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java new file mode 100644 index 00000000000000..a526a346c65800 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java @@ -0,0 +1,52 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +public class SystemPropsAsBuildTimeConfigSourceTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testBasicMultiModuleBuild() throws Exception { + + final File projectDir = getProjectDir("system-props-as-build-time-config-source"); + + final File appProperties = new File(projectDir, "application/gradle.properties"); + final File extensionProperties = new File(projectDir, "extensions/example-extension/gradle.properties"); + + final Path projectProperties = projectDir.toPath().resolve("gradle.properties"); + + try { + Files.copy(projectProperties, appProperties.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy(projectProperties, extensionProperties.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new IllegalStateException("Unable to copy gradle.properties file", e); + } + + gradleConfigurationCache(false); + runGradleWrapper(projectDir, + "-Dquarkus.example.name=cheburashka", + "-Dquarkus.example.runtime-name=crocodile", + "-Dquarkus.package.type=mutable-jar", + ":example-extension:example-extension-deployment:build", + // this quarkusIntTest will make sure runtime config properties passed as env vars when launching the app are effective + ":application:quarkusIntTest"); + + final Path buildSystemPropsPath = projectDir.toPath().resolve("application").resolve("build").resolve("quarkus-app") + .resolve("quarkus").resolve("build-system.properties"); + assertThat(buildSystemPropsPath).exists(); + var props = new Properties(); + try (var reader = Files.newBufferedReader(buildSystemPropsPath)) { + props.load(reader); + } + assertThat(props).doesNotContainKey("quarkus.example.name"); + assertThat(props).doesNotContainKey("quarkus.example.runtime-name"); + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java index 28c01e9c43934c..cf7904ca9b6a02 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java @@ -20,7 +20,7 @@ public class BasicJavaNativeBuildIT extends QuarkusNativeGradleITBase { public void shouldBuildNativeImage() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); - final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.type=fast-jar"); + final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative"); assertThat(build.getTasks().get(":quarkusBuild")).isEqualTo(BuildResult.SUCCESS_OUTCOME); final String buildOutput = build.getOutput(); @@ -48,7 +48,7 @@ public void shouldBuildNativeImage() throws Exception { public void shouldBuildNativeImageWithCustomName() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); - final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.type=fast-jar", + final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.output-name=test"); assertThat(build.getTasks().get(":quarkusBuild")).isEqualTo(BuildResult.SUCCESS_OUTCOME); @@ -78,7 +78,7 @@ public void shouldBuildNativeImageWithCustomName() throws Exception { public void shouldBuildNativeImageWithCustomNameWithoutSuffix() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); - final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.type=fast-jar", + final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.output-name=test", "-Dquarkus.package.add-runner-suffix=false"); assertThat(build.getTasks().get(":quarkusBuild")).isEqualTo(BuildResult.SUCCESS_OUTCOME); From 20d40b18b9957caccb31e829e17e4c977e08b589 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 13 Nov 2023 17:23:10 +0100 Subject: [PATCH 58/68] Use dynamically resolved Java version when creating Gradle projects Also add tests for Java 21. Fixes #37047 --- .../base/build-layout.include.qute | 9 +---- .../scala/build.tpl.qute.gradle.kts | 9 +---- .../gradle/base/build-layout.include.qute | 9 +---- .../gradle/scala/build.tpl.qute.gradle | 9 +---- .../quarkus/maven/it/CreateProjectMojoIT.java | 37 +++++++++++++++++++ 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/base/build-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/base/build-layout.include.qute index 3b643510d09a4d..4b0d7655a7a459 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/base/build-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/base/build-layout.include.qute @@ -50,13 +50,8 @@ version = "{project.version}" {#insert java} java { - {#if java.version == "17"} - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - {#else} - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - {/if} + sourceCompatibility = JavaVersion.VERSION_{java.version} + targetCompatibility = JavaVersion.VERSION_{java.version} } {/} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/scala/build.tpl.qute.gradle.kts b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/scala/build.tpl.qute.gradle.kts index a6c4baa92a1a30..035088350b60fe 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/scala/build.tpl.qute.gradle.kts +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/scala/build.tpl.qute.gradle.kts @@ -9,11 +9,6 @@ plugins { tasks.withType { scalaCompileOptions.encoding = "UTF-8" - {#if java.version == "11"} - sourceCompatibility = JavaVersion.VERSION_11.toString() - targetCompatibility = JavaVersion.VERSION_11.toString() - {#else} - sourceCompatibility = JavaVersion.VERSION_1_8.toString() - targetCompatibility = JavaVersion.VERSION_1_8.toString() - {/if} + sourceCompatibility = JavaVersion.VERSION_{java.version}.toString() + targetCompatibility = JavaVersion.VERSION_{java.version}.toString() } diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/base/build-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/base/build-layout.include.qute index 53c267137122b8..dd292c9d2fbd69 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/base/build-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/base/build-layout.include.qute @@ -46,13 +46,8 @@ version '{project.version}' {#insert java} java { - {#if java.version == "17"} - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - {#else} - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - {/if} + sourceCompatibility = JavaVersion.VERSION_{java.version} + targetCompatibility = JavaVersion.VERSION_{java.version} } {/} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/scala/build.tpl.qute.gradle b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/scala/build.tpl.qute.gradle index 6119eea5168af0..835921cebee0b2 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/scala/build.tpl.qute.gradle +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/scala/build.tpl.qute.gradle @@ -9,11 +9,6 @@ plugins { compileScala { scalaCompileOptions.encoding = 'UTF-8' - {#if java.version == "11"} - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - {#else} - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - {/if} + sourceCompatibility = JavaVersion.VERSION_{java.version} + targetCompatibility = JavaVersion.VERSION_{java.version} } diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateProjectMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateProjectMojoIT.java index 39ba3756cf3aff..8374d41fe2a172 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateProjectMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateProjectMojoIT.java @@ -494,6 +494,24 @@ public void testProjectGenerationFromScratchWithJava17() throws MavenInvocationE .contains("maven.compiler.release>17<"); } + @Test + public void testProjectGenerationFromScratchWithJava21() throws MavenInvocationException, IOException { + testDir = initEmptyProject("projects/project-generation-with-java21"); + assertThat(testDir).isDirectory(); + invoker = initInvoker(testDir); + + Properties properties = new Properties(); + properties.put("javaVersion", "21"); + + InvocationResult result = setup(properties); + assertThat(result.getExitCode()).isZero(); + + testDir = new File(testDir, "code-with-quarkus"); + assertThat(new File(testDir, "pom.xml")).isFile(); + assertThat(FileUtils.readFileToString(new File(testDir, "pom.xml"), "UTF-8")) + .contains("maven.compiler.release>21<"); + } + @Test public void testProjectGenerationFromScratchWithGradleJava11() throws MavenInvocationException, IOException { testDir = initEmptyProject("projects/project-generation-with-gradle-java11"); @@ -532,6 +550,25 @@ public void testProjectGenerationFromScratchWithGradleJava17() throws MavenInvoc .contains("sourceCompatibility = JavaVersion.VERSION_17"); } + @Test + public void testProjectGenerationFromScratchWithGradleJava21() throws MavenInvocationException, IOException { + testDir = initEmptyProject("projects/project-generation-with-gradle-java21"); + assertThat(testDir).isDirectory(); + invoker = initInvoker(testDir); + + Properties properties = new Properties(); + properties.put("javaVersion", "21"); + properties.put("buildTool", "gradle"); + + InvocationResult result = setup(properties); + assertThat(result.getExitCode()).isZero(); + + testDir = new File(testDir, "code-with-quarkus"); + assertThat(new File(testDir, "build.gradle")).isFile(); + assertThat(FileUtils.readFileToString(new File(testDir, "build.gradle"), "UTF-8")) + .contains("sourceCompatibility = JavaVersion.VERSION_21"); + } + /** * Reproducer for https://github.com/quarkusio/quarkus/issues/671 */ From 30173e8d525a3d5fb0849ec7026aa1029ccdaef0 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 10 Nov 2023 16:29:06 +0100 Subject: [PATCH 59/68] Qute: allow extensions to register additional template roots - via TemplateRootBuildItem - the default root is "templates" --- .../qute/deployment/QuteProcessor.java | 114 +++++++++++------- .../deployment/TemplateRootBuildItem.java | 36 ++++++ .../deployment/TemplateRootsBuildItem.java | 72 +++++++++++ .../AdditionalTemplateRootTest.java | 57 +++++++++ .../TemplateRootBuildItemTest.java | 18 +++ .../TemplateRootsBuildItemTest.java | 39 ++++++ .../quarkus/qute/runtime/EngineProducer.java | 53 ++++---- .../io/quarkus/qute/runtime/QuteRecorder.java | 9 +- 8 files changed, 325 insertions(+), 73 deletions(-) create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootBuildItem.java create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootsBuildItem.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootBuildItemTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootsBuildItemTest.java diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 8497e70b4dedfb..adc3c78166a5c9 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -160,7 +160,7 @@ public class QuteProcessor { private static final String CHECKED_TEMPLATE_BASE_PATH = "basePath"; private static final String CHECKED_TEMPLATE_DEFAULT_NAME = "defaultName"; private static final String IGNORE_FRAGMENTS = "ignoreFragments"; - private static final String BASE_PATH = "templates"; + private static final String DEFAULT_ROOT_PATH = "templates"; private static final Set ITERATION_METADATA_KEYS = Set.of("count", "index", "indexParity", "hasNext", "odd", "isOdd", "even", "isEven", "isLast", "isFirst"); @@ -184,6 +184,20 @@ FeatureBuildItem feature() { return new FeatureBuildItem(Feature.QUTE); } + @BuildStep + TemplateRootBuildItem defaultTemplateRoot() { + return new TemplateRootBuildItem(DEFAULT_ROOT_PATH); + } + + @BuildStep + TemplateRootsBuildItem collectTemplateRoots(List templateRoots) { + Set roots = new HashSet<>(); + for (TemplateRootBuildItem root : templateRoots) { + roots.add(root.getPath()); + } + return new TemplateRootsBuildItem(Set.copyOf(roots)); + } + @BuildStep List beanDefiningAnnotations() { return List.of( @@ -2045,9 +2059,9 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchives, BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources, - QuteConfig config) + QuteConfig config, + TemplateRootsBuildItem templateRoots) throws IOException { - Set basePaths = new HashSet<>(); Set allApplicationArchives = applicationArchives.getAllApplicationArchives(); List extensionArtifacts = curateOutcome.getApplicationModel().getDependencies().stream() .filter(Dependency::isRuntimeExtensionArtifact).collect(Collectors.toList()); @@ -2056,7 +2070,12 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchives, watchedPaths.produce(HotDeploymentWatchedFileBuildItem.builder().setLocationPredicate(new Predicate() { @Override public boolean test(String path) { - return path.startsWith(BASE_PATH); + for (String rootPath : templateRoots) { + if (path.startsWith(rootPath)) { + return true; + } + } + return false; } }).build()); @@ -2065,54 +2084,67 @@ public boolean test(String path) { // Skip extension archives that are also application archives continue; } - for (Path path : artifact.getResolvedPaths()) { - if (Files.isDirectory(path)) { - // Try to find the templates in the root dir - try (Stream paths = Files.list(path)) { - Path basePath = paths.filter(QuteProcessor::isBasePath).findFirst().orElse(null); - if (basePath != null) { - LOGGER.debugf("Found extension templates dir: %s", path); - scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources, - config); - break; - } - } + for (Path resolvedPath : artifact.getResolvedPaths()) { + if (Files.isDirectory(resolvedPath)) { + scanPath(resolvedPath, resolvedPath, config, templateRoots, watchedPaths, templatePaths, + nativeImageResources); } else { - try (FileSystem artifactFs = ZipUtils.newFileSystem(path)) { - Path basePath = artifactFs.getPath(BASE_PATH); - if (Files.exists(basePath)) { - LOGGER.debugf("Found extension templates in: %s", path); - scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources, - config); + try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedPath)) { + for (String templateRoot : templateRoots) { + Path artifactBasePath = artifactFs.getPath(templateRoot); + if (Files.exists(artifactBasePath)) { + LOGGER.debugf("Found extension templates in: %s", resolvedPath); + scan(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, templatePaths, + nativeImageResources, + config); + } } } catch (IOException e) { - LOGGER.warnf(e, "Unable to create the file system from the path: %s", path); + LOGGER.warnf(e, "Unable to create the file system from the path: %s", resolvedPath); } } } } for (ApplicationArchive archive : allApplicationArchives) { archive.accept(tree -> { - for (Path rootDir : tree.getRoots()) { + for (Path root : tree.getRoots()) { // Note that we cannot use ApplicationArchive.getChildPath(String) here because we would not be able to detect // a wrong directory name on case-insensitive file systems - try (Stream rootDirPaths = Files.list(rootDir)) { - Path basePath = rootDirPaths.filter(QuteProcessor::isBasePath).findFirst().orElse(null); - if (basePath != null) { - LOGGER.debugf("Found templates dir: %s", basePath); - basePaths.add(basePath); - scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources, - config); - break; - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } + scanPath(root, root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); } }); } } + private void scanPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots, + BuildProducer watchedPaths, + BuildProducer templatePaths, + BuildProducer nativeImageResources) { + if (!Files.isDirectory(path)) { + return; + } + try (Stream paths = Files.list(path)) { + for (Path file : paths.collect(Collectors.toList())) { + if (Files.isDirectory(file)) { + // Iterate over the directories in the root + // "/io", "/META-INF", "/templates", "/web", etc. + Path relativePath = rootPath.relativize(file); + if (templateRoots.isRoot(relativePath)) { + LOGGER.debugf("Found templates dir: %s", file); + scan(file, file, file.getFileName() + "/", watchedPaths, templatePaths, + nativeImageResources, + config); + } else if (templateRoots.maybeRoot(relativePath)) { + // Scan the path recursively because the template root may be nested, for example "/web/public" + scanPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); + } + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + @BuildStep TemplateFilePathsBuildItem collectTemplateFilePaths(QuteConfig config, List templatePaths) { Set filePaths = new HashSet(); @@ -2367,7 +2399,8 @@ public boolean test(TypeCheck check) { void initialize(BuildProducer syntheticBeans, QuteRecorder recorder, List generatedValueResolvers, List templatePaths, Optional templateVariants, - List templateInitializers) { + List templateInitializers, + TemplateRootsBuildItem templateRoots) { List templates = new ArrayList<>(); List tags = new ArrayList<>(); @@ -2391,7 +2424,8 @@ void initialize(BuildProducer syntheticBeans, QuteRecord .supplier(recorder.createContext(generatedValueResolvers.stream() .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), templates, tags, variants, templateInitializers.stream() - .map(GeneratedTemplateInitializerBuildItem::getClassName).collect(Collectors.toList()))) + .map(GeneratedTemplateInitializerBuildItem::getClassName).collect(Collectors.toList()), + templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()))) .done()); } @@ -3353,10 +3387,6 @@ private static boolean isExcluded(TypeCheck check, Iterable return false; } - private static boolean isBasePath(Path path) { - return path.getFileName().toString().equals(BASE_PATH); - } - private void checkDuplicatePaths(List templatePaths) { Map> duplicates = templatePaths.stream() .collect(Collectors.groupingBy(TemplatePathBuildItem::getPath)); diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootBuildItem.java new file mode 100644 index 00000000000000..4ce1258e48ac37 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootBuildItem.java @@ -0,0 +1,36 @@ +package io.quarkus.qute.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * This build item represents a source of template files. + *

+ * By default, the templates are found in the {@code templates} directory. However, an extension can produce this build item to + * register an additional root path. + *

+ * The path is relative to the artifact/project root and OS-agnostic, i.e. {@code /} is used as a path separator. + */ +public final class TemplateRootBuildItem extends MultiBuildItem { + + private final String path; + + public TemplateRootBuildItem(String path) { + this.path = normalize(path); + } + + public String getPath() { + return path; + } + + static String normalize(String path) { + path = path.strip(); + if (path.startsWith("/")) { + path = path.substring(1); + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return path; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootsBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootsBuildItem.java new file mode 100644 index 00000000000000..a6750aa200976d --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootsBuildItem.java @@ -0,0 +1,72 @@ +package io.quarkus.qute.deployment; + +import java.io.File; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * The set of template root paths. + */ +public final class TemplateRootsBuildItem extends SimpleBuildItem implements Iterable { + + private Set rootPaths; + + public TemplateRootsBuildItem(Set paths) { + this.rootPaths = paths; + } + + public Set getPaths() { + return rootPaths; + } + + @Override + public Iterator iterator() { + return rootPaths.iterator(); + } + + /** + * The path must be relative to the resource root. + * + * @param path + * @return {@code true} is the given path represents a template root, {@code false} otherwise + */ + public boolean isRoot(Path path) { + String pathStr = normalize(path); + for (String rootPath : rootPaths) { + if (pathStr.equals(rootPath)) { + return true; + } + } + return false; + } + + /** + * The path must be relative to the resource root. + * + * @param path + * @return {@code true} is the given path may represent a template root, {@code false} otherwise + */ + public boolean maybeRoot(Path path) { + String pathStr = normalize(path); + for (String rootPath : rootPaths) { + if ((rootPath.contains("/") && rootPath.startsWith(pathStr)) + || rootPath.equals(pathStr)) { + return true; + } + } + return false; + } + + private static String normalize(Path path) { + String pathStr = path.toString(); + if (File.separatorChar != '/') { + // \foo\bar\templates -> /foo/bar/templates + pathStr = pathStr.replace(File.separatorChar, '/'); + } + return TemplateRootBuildItem.normalize(pathStr); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java new file mode 100644 index 00000000000000..202cdbfc8b7e46 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java @@ -0,0 +1,57 @@ +package io.quarkus.qute.deployment.templateroot; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.function.Consumer; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.qute.Engine; +import io.quarkus.qute.Template; +import io.quarkus.qute.deployment.TemplateRootBuildItem; +import io.quarkus.test.QuarkusUnitTest; + +public class AdditionalTemplateRootTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt") + .addAsResource(new StringAsset("Hello {name}!"), "web/public/hello.txt")) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(new TemplateRootBuildItem("web/public")); + } + }).produces(TemplateRootBuildItem.class) + .build(); + } + }; + } + + @Inject + Template hello; + + @Inject + Engine engine; + + @Test + public void testTemplate() { + assertEquals("Hi M!", engine.getTemplate("hi").data("name", "M").render()); + assertEquals("Hello M!", hello.data("name", "M").render()); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootBuildItemTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootBuildItemTest.java new file mode 100644 index 00000000000000..5e7a0a1152ed43 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootBuildItemTest.java @@ -0,0 +1,18 @@ +package io.quarkus.qute.deployment.templateroot; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import io.quarkus.qute.deployment.TemplateRootBuildItem; + +public class TemplateRootBuildItemTest { + + @Test + public void testNormalizedName() { + assertEquals("foo", new TemplateRootBuildItem("/foo/ ").getPath()); + assertEquals("foo/bar", new TemplateRootBuildItem("/foo/bar").getPath()); + assertEquals("baz", new TemplateRootBuildItem(" baz/").getPath()); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootsBuildItemTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootsBuildItemTest.java new file mode 100644 index 00000000000000..f2171a2b1bd176 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootsBuildItemTest.java @@ -0,0 +1,39 @@ +package io.quarkus.qute.deployment.templateroot; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.nio.file.Path; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.quarkus.qute.deployment.TemplateRootsBuildItem; + +public class TemplateRootsBuildItemTest { + + @Test + public void testIsRoot() { + TemplateRootsBuildItem buildItem = new TemplateRootsBuildItem(Set.of("templates", "public/web")); + assertTrue(buildItem.isRoot(Path.of("/templates"))); + assertTrue(buildItem.isRoot(Path.of("public/web"))); + assertTrue(buildItem.isRoot(Path.of("/templates/"))); + assertTrue(buildItem.isRoot(Path.of("public/web/"))); + assertFalse(buildItem.isRoot(Path.of("/foo/templates"))); + assertFalse(buildItem.isRoot(Path.of("/web"))); + assertFalse(buildItem.isRoot(Path.of("public"))); + assertFalse(buildItem.isRoot(Path.of("baz/web"))); + assertFalse(buildItem.isRoot(Path.of("baz/template"))); + } + + @Test + public void testMaybeRoot() { + TemplateRootsBuildItem buildItem = new TemplateRootsBuildItem(Set.of("templates", "public/web")); + assertTrue(buildItem.maybeRoot(Path.of("public"))); + assertTrue(buildItem.maybeRoot(Path.of("templates"))); + assertTrue(buildItem.maybeRoot(Path.of(File.separatorChar + "public" + File.separatorChar))); + assertFalse(buildItem.maybeRoot(Path.of("template"))); + assertFalse(buildItem.maybeRoot(Path.of("foo"))); + } +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index c4633c9028a563..9bcf522449dc66 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; @@ -75,8 +76,7 @@ public class EngineProducer { private final ContentTypes contentTypes; private final List tags; private final List suffixes; - private final String basePath; - private final String tagPath; + private final Set templateRoots; private final Pattern templatePathExclude; private final Locale defaultLocale; private final Charset defaultCharset; @@ -89,8 +89,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig @All List namespaceResolvers) { this.contentTypes = contentTypes; this.suffixes = config.suffixes; - this.basePath = "templates/"; - this.tagPath = basePath + TAGS; + this.templateRoots = context.getTemplateRoots(); this.tags = context.getTags(); this.templatePathExclude = config.templatePathExclude; this.defaultLocale = locales.defaultLocale; @@ -300,14 +299,6 @@ void onShutdown(@Observes ShutdownEvent event) { Qute.clearCache(); } - String getBasePath() { - return basePath; - } - - String getTagPath() { - return tagPath; - } - private Resolver createResolver(String resolverClassName) { try { Class resolverClazz = Thread.currentThread() @@ -337,29 +328,31 @@ private TemplateInstance.Initializer createInitializer(String initializerClassNa } private Optional locate(String path) { - URL resource = null; - String templatePath = basePath + path; - LOGGER.debugf("Locate template for %s", templatePath); if (templatePathExclude.matcher(path).matches()) { return Optional.empty(); } - resource = locatePath(templatePath); - if (resource == null) { - // Try path with suffixes - for (String suffix : suffixes) { - String pathWithSuffix = path + "." + suffix; - if (templatePathExclude.matcher(pathWithSuffix).matches()) { - continue; - } - templatePath = basePath + pathWithSuffix; - resource = locatePath(templatePath); - if (resource != null) { - break; + for (String templateRoot : templateRoots) { + URL resource = null; + String templatePath = templateRoot + path; + LOGGER.debugf("Locate template for %s", templatePath); + resource = locatePath(templatePath); + if (resource == null) { + // Try path with suffixes + for (String suffix : suffixes) { + String pathWithSuffix = path + "." + suffix; + if (templatePathExclude.matcher(pathWithSuffix).matches()) { + continue; + } + templatePath = templateRoot + pathWithSuffix; + resource = locatePath(templatePath); + if (resource != null) { + break; + } } } - } - if (resource != null) { - return Optional.of(new ResourceTemplateLocation(resource, createVariant(templatePath))); + if (resource != null) { + return Optional.of(new ResourceTemplateLocation(resource, createVariant(templatePath))); + } } return Optional.empty(); } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java index be9e699c827daa..17a2caee2ddba9 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Supplier; import io.quarkus.runtime.annotations.Recorder; @@ -11,7 +12,7 @@ public class QuteRecorder { public Supplier createContext(List resolverClasses, List templatePaths, List tags, Map> variants, - List templateInstanceInitializerClasses) { + List templateInstanceInitializerClasses, Set templateRoots) { return new Supplier() { @Override @@ -43,6 +44,10 @@ public List getTemplateInstanceInitializerClasses() { return templateInstanceInitializerClasses; } + @Override + public Set getTemplateRoots() { + return templateRoots; + } }; } }; @@ -60,6 +65,8 @@ public interface QuteContext { List getTemplateInstanceInitializerClasses(); + Set getTemplateRoots(); + } } From 527f754925b0aae89292090f9718e1185344af0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:20:42 +0000 Subject: [PATCH 60/68] Bump io.smallrye.config:smallrye-config-source-yaml in /devtools/gradle Bumps io.smallrye.config:smallrye-config-source-yaml from 3.4.1 to 3.4.4. --- updated-dependencies: - dependency-name: io.smallrye.config:smallrye-config-source-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- devtools/gradle/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index e07d048c4e6754..9902875d78e44e 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -3,7 +3,7 @@ plugin-publish = "1.2.1" # updating Kotlin here makes QuarkusPluginTest > shouldNotFailOnProjectDependenciesWithoutMain(Path) fail kotlin = "1.9.20" -smallrye-config = "3.4.1" +smallrye-config = "3.4.4" junit5 = "5.10.1" assertj = "3.24.2" From 732048ccefb75528af262b5743778a647ea8309b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:55:04 +0000 Subject: [PATCH 61/68] Bump com.fasterxml.jackson:jackson-bom from 2.15.2 to 2.15.3 Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.15.2 to 2.15.3. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.15.2...jackson-bom-2.15.3) --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- independent-projects/extension-maven-plugin/pom.xml | 2 +- independent-projects/resteasy-reactive/pom.xml | 2 +- independent-projects/tools/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index cd6a22dcd59262..43a373c25990fc 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -92,7 +92,7 @@ 2.1.0 23.0.1 1.7.0 - 2.15.2 + 2.15.3 1.0.0.Final 3.13.0 1.16.0 diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index 8e928fdce58dc9..165bdf86493a83 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -42,7 +42,7 @@ 3.2.1 3.1.2 3.8.1 - 2.15.2 + 2.15.3 1.3.2 5.10.0 diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 99d84bb8440025..82ecd9308c3376 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -66,7 +66,7 @@ 4.4.6 5.3.2 1.0.0.Final - 2.15.2 + 2.15.3 2.4.0 3.0.2 3.0.3 diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index 624be6575399f4..48814923bfe7a6 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -52,7 +52,7 @@ 3.24.2 - 2.15.2 + 2.15.3 4.0.1 5.10.0 1.24.0 From a26e469dc60593f7df5f2fb8fc848818b4d27dc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:57:24 +0000 Subject: [PATCH 62/68] Bump io.smallrye.reactive:mutiny from 2.2.0 to 2.5.1 Bumps [io.smallrye.reactive:mutiny](https://github.com/smallrye/smallrye-mutiny) from 2.2.0 to 2.5.1. - [Release notes](https://github.com/smallrye/smallrye-mutiny/releases) - [Commits](https://github.com/smallrye/smallrye-mutiny/compare/2.2.0...2.5.1) --- updated-dependencies: - dependency-name: io.smallrye.reactive:mutiny dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- independent-projects/arc/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index bd1b11af521983..5219daa4e1bce7 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -50,7 +50,7 @@ 1.7.0 3.1.5 3.5.3.Final - 2.2.0 + 2.5.1 1.6.Final 3.24.2 From 1fa6022798e3d1e506f7593f1d5351ce014ded4f Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Tue, 14 Nov 2023 10:15:34 +1100 Subject: [PATCH 63/68] Add endpoints page in Dev UI Signed-off-by: Phillip Kruger --- .../src/main/resources/dev-ui/qwc-info.js | 4 +- .../devui/deployment/DevUIProcessor.java | 7 ++ .../deployment/menu/EndpointsProcessor.java | 57 ++++++++++ .../resources/dev-ui/qwc/qwc-endpoints.js | 101 ++++++++++++++++++ .../quarkus/devui/runtime/DevUIRecorder.java | 8 ++ .../quarkus/devui/runtime/EndpointInfo.java | 36 +++++++ .../devui/runtime/EndpointInfoHandler.java | 79 ++++++++++++++ 7 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfo.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfoHandler.java diff --git a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js index 5960477bd1aa82..a3f8d214766862 100644 --- a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js +++ b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js @@ -56,7 +56,7 @@ export class QwcInfo extends LitElement { } async load() { - const response = await fetch(this._infoUrl) + const response = await fetch(this._infoUrl); const data = await response.json(); this._info = data; } @@ -72,7 +72,7 @@ export class QwcInfo extends LitElement { }else{ return html`
-
Fetching infomation...
+
Fetching information...
`; diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java index f8996848d6af63..b2468365265e9f 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java @@ -201,6 +201,13 @@ void registerDevUiHandlers( .build()); } + Handler endpointInfoHandler = recorder.endpointInfoHandler(basepath); + + routeProducer.produce( + nonApplicationRootPathBuildItem.routeBuilder().route(DEVUI + SLASH + "endpoints.json") + .handler(endpointInfoHandler) + .build()); + // For the Vaadin router (So that bookmarks/url refreshes work) for (DevUIRoutesBuildItem devUIRoutesBuildItem : devUIRoutesBuildItems) { String route = devUIRoutesBuildItem.getPath(); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java new file mode 100644 index 00000000000000..634bdf50620323 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java @@ -0,0 +1,57 @@ +package io.quarkus.devui.deployment.menu; + +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + +import java.util.List; +import java.util.stream.Collectors; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.devui.deployment.InternalPageBuildItem; +import io.quarkus.devui.runtime.DevUIRecorder; +import io.quarkus.devui.runtime.EndpointInfo; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; + +/** + * This creates Endpoints Page + */ +public class EndpointsProcessor { + private static final String DEVUI = "dev-ui"; + + @Record(STATIC_INIT) + @BuildStep(onlyIf = IsDevelopment.class) + void addEndpointInfos(List displayableEndpoints, + DevUIRecorder recorder, HttpRootPathBuildItem httpRoot) { + + List endpoints = displayableEndpoints + .stream() + .map(v -> new EndpointInfo(v.getEndpoint(httpRoot), v.getDescription())) + .sorted() + .collect(Collectors.toList()); + + recorder.setEndpoints(endpoints); + } + + @BuildStep(onlyIf = IsDevelopment.class) + InternalPageBuildItem createEndpointsPage(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { + + String basepath = nonApplicationRootPathBuildItem.resolvePath(DEVUI); + + InternalPageBuildItem endpointsPage = new InternalPageBuildItem("Endpoints", 25); + + endpointsPage.addBuildTimeData("basepath", basepath); + + // Page + endpointsPage.addPage(Page.webComponentPageBuilder() + .namespace("devui-endpoints") + .title("Endpoints") + .icon("font-awesome-solid:plug") + .componentLink("qwc-endpoints.js")); + + return endpointsPage; + } +} diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js new file mode 100644 index 00000000000000..9b0ded9264cbb3 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js @@ -0,0 +1,101 @@ +import { LitElement, html, css} from 'lit'; +import { basepath } from 'devui-data'; +import '@vaadin/progress-bar'; +import '@vaadin/grid'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +/** + * This component show all available endpoints + */ +export class QwcEndpoints extends LitElement { + + static styles = css` + .infogrid { + width: 99%; + height: 99%; + } + a { + cursor: pointer; + color: var(--lumo-body-text-color); + } + a:link { + text-decoration: none; + color: var(--lumo-body-text-color); + } + a:visited { + text-decoration: none; + color: var(--lumo-body-text-color); + } + a:active { + text-decoration: none; + color: var(--lumo-body-text-color); + } + a:hover { + color: var(--quarkus-red); + } + `; + + static properties = { + _info: {state: true}, + } + + constructor() { + super(); + this._info = null; + } + + async connectedCallback() { + super.connectedCallback(); + await this.load(); + } + + async load() { + const response = await fetch(basepath + "/endpoints.json"); + const data = await response.json(); + this._info = data; + } + + render() { + if (this._info) { + const items = []; + for (const [key, value] of Object.entries(this._info)) { + items.push({"uri" : key, "description": value}); + } + + return html` + + + + + + `; + }else{ + return html` +
+
Fetching information...
+ +
+ `; + } + } + + _uriRenderer(endpoint) { + if (endpoint.uri) { + return html`${endpoint.uri}`; + } + } + + _descriptionRenderer(endpoint) { + if (endpoint.description) { + return html`${endpoint.description}`; + } + } + +} +customElements.define('qwc-endpoints', QwcEndpoints); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java index 57fc606b1b702d..9b9336d19253df 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java @@ -87,6 +87,14 @@ public Handler buildTimeStaticHandler(String basePath, Map endpointInfoHandler(String basePath) { + return new EndpointInfoHandler(basePath); + } + + public void setEndpoints(List endpointInfos) { + EndpointInfoHandler.setEndpoints(endpointInfos); + } + public Handler vaadinRouterHandler(String basePath) { return new VaadinRouterHandler(basePath); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfo.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfo.java new file mode 100644 index 00000000000000..73c9c3f4353bf6 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfo.java @@ -0,0 +1,36 @@ +package io.quarkus.devui.runtime; + +public class EndpointInfo implements Comparable { + private String uri; + private String description; + + // for bytecode recorder + public EndpointInfo() { + } + + public EndpointInfo(String uri, String description) { + this.uri = uri; + this.description = description; + } + + public String getDescription() { + return description; + } + + public String getUri() { + return uri; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setUri(String uri) { + this.uri = uri; + } + + @Override + public int compareTo(EndpointInfo o) { + return uri.compareTo(o.uri); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfoHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfoHandler.java new file mode 100644 index 00000000000000..b171187fdd22e6 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfoHandler.java @@ -0,0 +1,79 @@ +package io.quarkus.devui.runtime; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.vertx.core.Handler; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler to return the endpoint info + */ +public class EndpointInfoHandler implements Handler { + private static volatile List endpointInfos; + + static void setEndpoints(List endpointInfos) { + EndpointInfoHandler.endpointInfos = endpointInfos; + } + + private String basePath; // Like /q/dev-ui + + public EndpointInfoHandler() { + + } + + public EndpointInfoHandler(String basePath) { + this.basePath = basePath; + } + + public String getBasePath() { + return basePath; + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + @Override + public void handle(RoutingContext event) { + String normalizedPath = event.normalizedPath(); + if (normalizedPath.contains(SLASH)) { + int si = normalizedPath.lastIndexOf(SLASH) + 1; + String path = normalizedPath.substring(0, si); + String fileName = normalizedPath.substring(si); + if (path.startsWith(basePath) && fileName.equals("endpoints.json")) { + + event.response() + .setStatusCode(STATUS) + .setStatusMessage(OK) + .putHeader(CONTENT_TYPE, "application/json") + .end(Json.encodePrettily(getContent())); + + } else { + event.next(); + } + } else { + event.next(); + } + } + + private JsonObject getContent() { + + Map info = new HashMap<>(); + + for (EndpointInfo endpoint : EndpointInfoHandler.endpointInfos) { + info.put(endpoint.getUri(), endpoint.getDescription()); + } + + return new JsonObject(info); + + } + + private static final int STATUS = 200; + private static final String OK = "OK"; + private static final String SLASH = "/"; + private static final String CONTENT_TYPE = "Content-Type"; +} From 055978e05532e0aa53f16e360787927109ab0c9e Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Mon, 13 Nov 2023 13:47:02 +1100 Subject: [PATCH 64/68] OpenAPI make sure basic auth auto detection work Signed-off-by: Phillip Kruger --- .../vertx/http/deployment/HttpSecurityProcessor.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index 65ed375ee01cd4..e414b69b3f7a99 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -104,9 +104,10 @@ AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, .appliedToClass() .whenClass(cl -> BASIC_AUTH_MECH_NAME.equals(cl.name())) .thenTransform(t -> t.add(DEFAULT_BEAN)))); - if (buildTimeConfig.auth.basic.isPresent() && buildTimeConfig.auth.basic.get()) { - securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); - } + } + + if (buildTimeConfig.auth.basic.isPresent() && buildTimeConfig.auth.basic.get()) { + securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(BasicAuthenticationMechanism.class).build(); From bded5fea5cfcd7ce542950836383c9b72e759ff2 Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Tue, 14 Nov 2023 10:29:44 +0200 Subject: [PATCH 65/68] Add label area/native-image to issues mentioning mandrel As mandrel is now the default builder image, most issues about native contain mandrel in the body even if they are not mandrel specific. Start labeling the issues as `area/native-image` and let the mandrel team decide whether it's mandrel specific or not. --- .github/quarkus-github-bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index 3507f5cfbb5349..f0a3e9f48a1b61 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -407,7 +407,7 @@ triage: - extensions/google-cloud-functions - integration-tests/google-cloud-functions - id: mandrel - labels: [area/mandrel] + labels: [area/native-image] titleBody: "mandrel" notify: [galderz, zakkak, Karm] - id: native-image From 57a766faee70ebfbc280c8afe20b35eb76d75cf8 Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Tue, 14 Nov 2023 10:31:25 +0200 Subject: [PATCH 66/68] Notify zakkak about native-image issues and not graphics --- .github/quarkus-github-bot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index f0a3e9f48a1b61..3e580c63ce5843 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -413,7 +413,7 @@ triage: - id: native-image labels: [area/native-image] title: "\\bnative\\b" - notify: [] + notify: [zakkak] - id: awt labels: [area/graphics] expression: | @@ -421,7 +421,7 @@ triage: || matches("sun.java2d", titleBody) || matches("javax.imageio", titleBody) || matches("sun.awt", titleBody) - notify: [galderz, zakkak, Karm] + notify: [galderz, Karm] notifyInPullRequest: true directories: - extensions/awt/ From 278a9dc2418cb632f9f851f8c484b92a63d1422f Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Tue, 14 Nov 2023 10:11:53 +0100 Subject: [PATCH 67/68] Updates infinispan client intelligence section --- .../asciidoc/infinispan-client-reference.adoc | 24 +++++++++++++++---- docs/src/main/asciidoc/infinispan-client.adoc | 12 ++++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/infinispan-client-reference.adoc b/docs/src/main/asciidoc/infinispan-client-reference.adoc index 6299db50b3b6f8..22674f6f605b47 100644 --- a/docs/src/main/asciidoc/infinispan-client-reference.adoc +++ b/docs/src/main/asciidoc/infinispan-client-reference.adoc @@ -95,28 +95,42 @@ quarkus.infinispan-client.hosts=localhost:11222 <1> quarkus.infinispan-client.username=admin <2> quarkus.infinispan-client.password=password <3> - -quarkus.infinispan-client.client-intelligence=BASIC <4> ---- <1> Sets Infinispan Server address list, separated with commas <2> Sets the authentication username <3> Sets the authentication password -<4> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac. Alternatively, you can use uri connection by providing a single connection property [source,properties] ---- quarkus.infinispan-client.uri=hotrod://admin:password@localhost:11222 <1> -quarkus.infinispan-client.client-intelligence=BASIC <2> ---- <1> Sets Infinispan URI connection. The following properties will be ignored: hosts, username and password. -<2> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac [TIP] ==== Use Infinispan Dev Services to run a server and connect without configuration. ==== +=== Client intelligence +Infinispan client uses intelligence mechanisms to efficiently send requests to Infinispan Server clusters. +By default, the *HASH_DISTRIBUTION_AWARE* intelligence mechanism is enabled. +However, locally with Docker for Mac, you might experience connectivity issues. +In this case, configure the client intelligence to *BASIC*. + +Learn more in the https://infinispan.org/docs/stable/titles/hotrod_java/hotrod_java.html#hotrod-client-intelligence_hotrod-java-client[Infinispan documentation]. + +[source,properties] +---- +quarkus.infinispan-client.client-intelligence=BASIC <1> +---- +<1> Docker for Mac workaround. + +[IMPORTANT] +==== +Don't use *BASIC* in production environments by default, performance might be impacted. +==== + === Configuring backup clusters in Cross-Site Replication In High Availability production deployments, it is common to have multiple Infinispan Clusters that are distributed across various Data Centers worldwide. Infinispan offers the capability to connect these clusters and diff --git a/docs/src/main/asciidoc/infinispan-client.adoc b/docs/src/main/asciidoc/infinispan-client.adoc index 06e08e5624225a..3e8ccc48ca277f 100644 --- a/docs/src/main/asciidoc/infinispan-client.adoc +++ b/docs/src/main/asciidoc/infinispan-client.adoc @@ -305,14 +305,22 @@ Then, open the `src/main/resources/application.properties` file and add: %prod.quarkus.infinispan-client.username=admin <2> %prod.quarkus.infinispan-client.password=password <3> -## Docker 4 Mac workaround -%prod.quarkus.infinispan-client.client-intelligence=BASIC <4> +## Docker 4 Mac workaround. Uncomment only if you are using Docker for Mac. +## Read more about it in the Infinispan Reference Guide +# %prod.quarkus.infinispan-client.client-intelligence=BASIC <4> ---- <1> Sets Infinispan Server address list, separated with commas <2> Sets the authentication username <3> Sets the authentication password <4> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac. +[IMPORTANT] +==== +Client intelligence changes impact your performance in production. +Don't change the client intelligence unless strictly necessary for your case. +Read more in the xref:infinispan-client-reference.adoc[Infinispan Client extension reference guide]. +==== + == Packaging and running in JVM mode You can run the application as a conventional jar file. From af65f8a3ee773738515944e2f07ea4827f26f6ee Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 7 Nov 2023 22:52:27 +0000 Subject: [PATCH 68/68] Support for dynamic OIDC JWK set resolution --- .../common/OidcRequestContextProperties.java | 14 +- extensions/oidc/runtime/pom.xml | 5 + .../io/quarkus/oidc/OidcTenantConfig.java | 75 ++++++++ .../runtime/BackChannelLogoutTokenCache.java | 90 +-------- ...efaultTokenIntrospectionUserInfoCache.java | 130 +++---------- .../DynamicVerificationKeyResolver.java | 177 ++++++++++++++++++ .../quarkus/oidc/runtime/JsonWebKeySet.java | 14 +- .../io/quarkus/oidc/runtime/MemoryCache.java | 135 +++++++++++++ .../oidc/runtime/OidcIdentityProvider.java | 24 ++- .../io/quarkus/oidc/runtime/OidcProvider.java | 80 +++++--- .../oidc/runtime/OidcProviderClient.java | 22 ++- .../io/quarkus/oidc/runtime/OidcRecorder.java | 11 +- .../quarkus/oidc/runtime/MemoryCacheTest.java | 101 ++++++++++ .../it/keycloak/OidcRequestCustomizer.java | 18 +- .../src/main/resources/application.properties | 1 + .../BearerTokenAuthorizationTest.java | 4 +- 16 files changed, 668 insertions(+), 233 deletions(-) create mode 100644 extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java create mode 100644 extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/MemoryCache.java create mode 100644 extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/MemoryCacheTest.java diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java index cdf75431cd09d1..d7a1f620a48af7 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java @@ -4,13 +4,25 @@ public class OidcRequestContextProperties { + public static String TOKEN = "token"; + public static String TOKEN_CREDENTIAL = "token_credential"; + private final Map properties; public OidcRequestContextProperties(Map properties) { this.properties = properties; } - public Object getProperty(String name) { + public Object get(String name) { return properties.get(name); } + + public String getString(String name) { + return (String) get(name); + } + + public T get(String name, Class type) { + return type.cast(get(name)); + } + } diff --git a/extensions/oidc/runtime/pom.xml b/extensions/oidc/runtime/pom.xml index 2833e27e5ffd9c..1c5145df46d567 100644 --- a/extensions/oidc/runtime/pom.xml +++ b/extensions/oidc/runtime/pom.xml @@ -50,6 +50,11 @@ quarkus-junit5-internal test + + org.awaitility + awaitility + test + diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index db4eea48e8d00d..ccee7c9ca76c50 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -381,6 +381,81 @@ public void setCleanUpTimerInterval(Duration cleanUpTimerInterval) { } } + /** + * Configuration for controlling how JsonWebKeySet containing verification keys should be acquired and managed. + */ + @ConfigItem + public Jwks jwks = new Jwks(); + + @ConfigGroup + public static class Jwks { + + /** + * If JWK verification keys should be fetched at the moment a connection to the OIDC provider + * is initialized. + *

+ * Disabling this property will delay the key acquisition until the moment the current token + * has to be verified. Typically it can only be necessary if the token or other telated request properties + * provide an additional context which is required to resolve the keys correctly. + */ + @ConfigItem(defaultValue = "true") + public boolean resolveEarly = true; + + /** + * Maximum number of JWK keys that can be cached. + * This property will be ignored if the {@link #resolveEarly} property is set to true. + */ + @ConfigItem(defaultValue = "10") + public int cacheSize = 10; + + /** + * Number of minutes a JWK key can be cached for. + * This property will be ignored if the {@link #resolveEarly} property is set to true. + */ + @ConfigItem(defaultValue = "10M") + public Duration cacheTimeToLive = Duration.ofMinutes(10); + + /** + * Cache timer interval. + * If this property is set then a timer will check and remove the stale entries periodically. + * This property will be ignored if the {@link #resolveEarly} property is set to true. + */ + @ConfigItem + public Optional cleanUpTimerInterval = Optional.empty(); + + public int getCacheSize() { + return cacheSize; + } + + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + } + + public Duration getCacheTimeToLive() { + return cacheTimeToLive; + } + + public void setCacheTimeToLive(Duration cacheTimeToLive) { + this.cacheTimeToLive = cacheTimeToLive; + } + + public Optional getCleanUpTimerInterval() { + return cleanUpTimerInterval; + } + + public void setCleanUpTimerInterval(Duration cleanUpTimerInterval) { + this.cleanUpTimerInterval = Optional.of(cleanUpTimerInterval); + } + + public boolean isResolveEarly() { + return resolveEarly; + } + + public void setResolveEarly(boolean resolveEarly) { + this.resolveEarly = resolveEarly; + } + } + @ConfigGroup public static class Frontchannel { /** diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java index 4150096851cf27..180c374ac7631a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java @@ -1,103 +1,33 @@ package io.quarkus.oidc.runtime; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; +import jakarta.enterprise.event.Observes; import io.quarkus.oidc.OidcTenantConfig; -import io.vertx.core.Handler; +import io.quarkus.runtime.ShutdownEvent; import io.vertx.core.Vertx; public class BackChannelLogoutTokenCache { - private OidcTenantConfig oidcConfig; - private Map cacheMap = new ConcurrentHashMap<>();; - private AtomicInteger size = new AtomicInteger(); + final MemoryCache cache; public BackChannelLogoutTokenCache(OidcTenantConfig oidcTenantConfig, Vertx vertx) { - this.oidcConfig = oidcTenantConfig; - init(vertx); - } - - private void init(Vertx vertx) { - cacheMap = new ConcurrentHashMap<>(); - if (oidcConfig.logout.backchannel.cleanUpTimerInterval.isPresent()) { - vertx.setPeriodic(oidcConfig.logout.backchannel.cleanUpTimerInterval.get().toMillis(), new Handler() { - @Override - public void handle(Long event) { - // Remove all the entries which have expired - removeInvalidEntries(); - } - }); - } + cache = new MemoryCache(vertx, oidcTenantConfig.logout.backchannel.cleanUpTimerInterval, + oidcTenantConfig.logout.backchannel.tokenCacheTimeToLive, oidcTenantConfig.logout.backchannel.tokenCacheSize); } public void addTokenVerification(String token, TokenVerificationResult result) { - if (!prepareSpaceForNewCacheEntry()) { - clearCache(); - } - cacheMap.put(token, new CacheEntry(result)); + cache.add(token, result); } public TokenVerificationResult removeTokenVerification(String token) { - CacheEntry entry = removeCacheEntry(token); - return entry == null ? null : entry.result; + return cache.remove(token); } public boolean containsTokenVerification(String token) { - return cacheMap.containsKey(token); - } - - public void clearCache() { - cacheMap.clear(); - size.set(0); - } - - private void removeInvalidEntries() { - long now = now(); - for (Iterator> it = cacheMap.entrySet().iterator(); it.hasNext();) { - Map.Entry next = it.next(); - if (isEntryExpired(next.getValue(), now)) { - it.remove(); - size.decrementAndGet(); - } - } - } - - private boolean prepareSpaceForNewCacheEntry() { - int currentSize; - do { - currentSize = size.get(); - if (currentSize == oidcConfig.logout.backchannel.tokenCacheSize) { - return false; - } - } while (!size.compareAndSet(currentSize, currentSize + 1)); - return true; + return cache.containsKey(token); } - private CacheEntry removeCacheEntry(String token) { - CacheEntry entry = cacheMap.remove(token); - if (entry != null) { - size.decrementAndGet(); - } - return entry; - } - - private boolean isEntryExpired(CacheEntry entry, long now) { - return entry.createdTime + oidcConfig.logout.backchannel.tokenCacheTimeToLive.toMillis() < now; - } - - private static long now() { - return System.currentTimeMillis(); - } - - private static class CacheEntry { - volatile TokenVerificationResult result; - long createdTime = System.currentTimeMillis(); - - public CacheEntry(TokenVerificationResult result) { - this.result = result; - } + void shutdown(@Observes ShutdownEvent event, Vertx vertx) { + cache.stopTimer(vertx); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java index 29c22f26430b50..214436d5e6988f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java @@ -1,10 +1,6 @@ package io.quarkus.oidc.runtime; -import java.util.Collections; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; +import jakarta.enterprise.event.Observes; import io.quarkus.oidc.OidcRequestContext; import io.quarkus.oidc.OidcTenantConfig; @@ -12,9 +8,8 @@ import io.quarkus.oidc.TokenIntrospectionCache; import io.quarkus.oidc.UserInfo; import io.quarkus.oidc.UserInfoCache; -import io.quarkus.oidc.runtime.OidcConfig.TokenCache; +import io.quarkus.runtime.ShutdownEvent; import io.smallrye.mutiny.Uni; -import io.vertx.core.Handler; import io.vertx.core.Vertx; /** @@ -31,43 +26,21 @@ public class DefaultTokenIntrospectionUserInfoCache implements TokenIntrospectio private static final Uni NULL_INTROSPECTION_UNI = Uni.createFrom().nullItem(); private static final Uni NULL_USERINFO_UNI = Uni.createFrom().nullItem(); - private TokenCache cacheConfig; - - private Map cacheMap; - private AtomicInteger size = new AtomicInteger(); + final MemoryCache cache; public DefaultTokenIntrospectionUserInfoCache(OidcConfig oidcConfig, Vertx vertx) { - this.cacheConfig = oidcConfig.tokenCache; - init(vertx); - } - - private void init(Vertx vertx) { - if (cacheConfig.maxSize > 0) { - cacheMap = new ConcurrentHashMap<>(); - if (cacheConfig.cleanUpTimerInterval.isPresent()) { - vertx.setPeriodic(cacheConfig.cleanUpTimerInterval.get().toMillis(), new Handler() { - @Override - public void handle(Long event) { - // Remove all the entries which have expired - removeInvalidEntries(); - } - }); - } - } else { - cacheMap = Collections.emptyMap(); - } + cache = new MemoryCache(vertx, oidcConfig.tokenCache.cleanUpTimerInterval, + oidcConfig.tokenCache.timeToLive, oidcConfig.tokenCache.maxSize); } @Override public Uni addIntrospection(String token, TokenIntrospection introspection, OidcTenantConfig oidcTenantConfig, OidcRequestContext requestContext) { - if (cacheConfig.maxSize > 0) { - CacheEntry entry = findValidCacheEntry(token); - if (entry != null) { - entry.introspection = introspection; - } else if (prepareSpaceForNewCacheEntry()) { - cacheMap.put(token, new CacheEntry(introspection)); - } + CacheEntry entry = cache.get(token); + if (entry != null) { + entry.introspection = introspection; + } else { + cache.add(token, new CacheEntry(introspection)); } return CodeAuthenticationMechanism.VOID_UNI; @@ -76,20 +49,18 @@ public Uni addIntrospection(String token, TokenIntrospection introspection @Override public Uni getIntrospection(String token, OidcTenantConfig oidcConfig, OidcRequestContext requestContext) { - CacheEntry entry = findValidCacheEntry(token); + CacheEntry entry = cache.get(token); return entry == null ? NULL_INTROSPECTION_UNI : Uni.createFrom().item(entry.introspection); } @Override public Uni addUserInfo(String token, UserInfo userInfo, OidcTenantConfig oidcTenantConfig, OidcRequestContext requestContext) { - if (cacheConfig.maxSize > 0) { - CacheEntry entry = findValidCacheEntry(token); - if (entry != null) { - entry.userInfo = userInfo; - } else if (prepareSpaceForNewCacheEntry()) { - cacheMap.put(token, new CacheEntry(userInfo)); - } + CacheEntry entry = cache.get(token); + if (entry != null) { + entry.userInfo = userInfo; + } else { + cache.add(token, new CacheEntry(userInfo)); } return CodeAuthenticationMechanism.VOID_UNI; @@ -98,67 +69,13 @@ public Uni addUserInfo(String token, UserInfo userInfo, OidcTenantConfig o @Override public Uni getUserInfo(String token, OidcTenantConfig oidcConfig, OidcRequestContext requestContext) { - CacheEntry entry = findValidCacheEntry(token); + CacheEntry entry = cache.get(token); return entry == null ? NULL_USERINFO_UNI : Uni.createFrom().item(entry.userInfo); } - public int getCacheSize() { - return cacheMap.size(); - } - - public void clearCache() { - cacheMap.clear(); - size.set(0); - } - - private void removeInvalidEntries() { - long now = now(); - for (Iterator> it = cacheMap.entrySet().iterator(); it.hasNext();) { - Map.Entry next = it.next(); - if (isEntryExpired(next.getValue(), now)) { - it.remove(); - size.decrementAndGet(); - } - } - } - - private boolean prepareSpaceForNewCacheEntry() { - int currentSize; - do { - currentSize = size.get(); - if (currentSize == cacheConfig.maxSize) { - return false; - } - } while (!size.compareAndSet(currentSize, currentSize + 1)); - return true; - } - - private CacheEntry findValidCacheEntry(String token) { - CacheEntry entry = cacheMap.get(token); - if (entry != null) { - long now = now(); - if (isEntryExpired(entry, now)) { - // Entry has expired, remote introspection will be required - entry = null; - cacheMap.remove(token); - size.decrementAndGet(); - } - } - return entry; - } - - private boolean isEntryExpired(CacheEntry entry, long now) { - return entry.createdTime + cacheConfig.timeToLive.toMillis() < now; - } - - private static long now() { - return System.currentTimeMillis(); - } - private static class CacheEntry { volatile TokenIntrospection introspection; volatile UserInfo userInfo; - long createdTime = System.currentTimeMillis(); public CacheEntry(TokenIntrospection introspection) { this.introspection = introspection; @@ -168,4 +85,17 @@ public CacheEntry(UserInfo userInfo) { this.userInfo = userInfo; } } + + public void clearCache() { + cache.clearCache(); + } + + public int getCacheSize() { + return cache.getCacheSize(); + } + + void shutdown(@Observes ShutdownEvent event, Vertx vertx) { + cache.stopTimer(vertx); + } + } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java new file mode 100644 index 00000000000000..f9b9eb7b2a03e4 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java @@ -0,0 +1,177 @@ +package io.quarkus.oidc.runtime; + +import java.security.Key; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import jakarta.enterprise.event.Observes; + +import org.jboss.logging.Logger; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwx.HeaderParameterNames; +import org.jose4j.jwx.JsonWebStructure; +import org.jose4j.keys.resolvers.VerificationKeyResolver; +import org.jose4j.lang.UnresolvableKeyException; + +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.security.credential.TokenCredential; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; + +public class DynamicVerificationKeyResolver { + private static final Logger LOG = Logger.getLogger(DynamicVerificationKeyResolver.class); + + private final OidcProviderClient client; + private final MemoryCache cache; + + public DynamicVerificationKeyResolver(OidcProviderClient client, OidcTenantConfig config) { + this.client = client; + this.cache = new MemoryCache(client.getVertx(), config.jwks.cleanUpTimerInterval, + config.jwks.cacheTimeToLive, config.jwks.cacheSize); + } + + public Uni resolve(TokenCredential tokenCred) { + JsonObject headers = OidcUtils.decodeJwtHeaders(tokenCred.getToken()); + Key key = findKeyInTheCache(headers); + if (key != null) { + return Uni.createFrom().item(new SingleKeyVerificationKeyResolver(key)); + } + + return client.getJsonWebKeySet(new OidcRequestContextProperties( + Map.of(OidcRequestContextProperties.TOKEN, tokenCred.getToken(), + OidcRequestContextProperties.TOKEN_CREDENTIAL, tokenCred))) + .onItem().transformToUni(new Function>() { + + @Override + public Uni apply(JsonWebKeySet jwks) { + Key newKey = null; + // Try 'kid' first + String kid = headers.getString(HeaderParameterNames.KEY_ID); + if (kid != null) { + newKey = getKeyWithId(jwks, kid); + if (newKey == null) { + // if `kid` was set then the key must exist + return Uni.createFrom().failure( + new UnresolvableKeyException(String.format("JWK with kid '%s' is not available", kid))); + } else { + cache.add(kid, newKey); + } + } + + String thumbprint = null; + if (newKey == null) { + thumbprint = headers.getString(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT); + if (thumbprint != null) { + newKey = getKeyWithS256Thumbprint(jwks, thumbprint); + if (newKey == null) { + // if only `x5tS256` was set then the key must exist + return Uni.createFrom().failure( + new UnresolvableKeyException(String.format( + "JWK with the SHA256 certificate thumbprint '%s' is not available", + thumbprint))); + } else { + cache.add(thumbprint, newKey); + } + } + } + + if (newKey == null) { + thumbprint = headers.getString(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT); + if (thumbprint != null) { + newKey = getKeyWithThumbprint(jwks, thumbprint); + if (newKey == null) { + // if only `x5t` was set then the key must exist + return Uni.createFrom().failure(new UnresolvableKeyException( + String.format("JWK with the certificate thumbprint '%s' is not available", + thumbprint))); + } else { + cache.add(thumbprint, newKey); + } + } + } + + if (newKey == null && kid == null && thumbprint == null) { + newKey = jwks.getKeyWithoutKeyIdAndThumbprint("RSA"); + } + + if (newKey == null) { + return Uni.createFrom().failure(new UnresolvableKeyException( + String.format( + "JWK is not available, neither 'kid' nor 'x5t#S256' nor 'x5t' token headers are set", + kid))); + } else { + return Uni.createFrom().item(new SingleKeyVerificationKeyResolver(newKey)); + } + } + + }); + } + + private static Key getKeyWithId(JsonWebKeySet jwks, String kid) { + if (kid != null) { + return jwks.getKeyWithId(kid); + } else { + LOG.debug("Token 'kid' header is not set"); + return null; + } + } + + private Key getKeyWithThumbprint(JsonWebKeySet jwks, String thumbprint) { + if (thumbprint != null) { + return jwks.getKeyWithThumbprint(thumbprint); + } else { + LOG.debug("Token 'x5t' header is not set"); + return null; + } + } + + private Key getKeyWithS256Thumbprint(JsonWebKeySet jwks, String thumbprint) { + if (thumbprint != null) { + return jwks.getKeyWithS256Thumbprint(thumbprint); + } else { + LOG.debug("Token 'x5tS256' header is not set"); + return null; + } + } + + private Key findKeyInTheCache(JsonObject headers) { + String kid = headers.getString(HeaderParameterNames.KEY_ID); + if (kid != null && cache.containsKey(kid)) { + return cache.get(kid); + } + String thumbprint = headers.getString(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT); + if (thumbprint != null && cache.containsKey(thumbprint)) { + return cache.get(thumbprint); + } + + thumbprint = headers.getString(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT); + if (thumbprint != null && cache.containsKey(thumbprint)) { + return cache.get(thumbprint); + } + + return null; + } + + static class SingleKeyVerificationKeyResolver implements VerificationKeyResolver { + + private Key key; + + SingleKeyVerificationKeyResolver(Key key) { + this.key = key; + } + + @Override + public Key resolveKey(JsonWebSignature jws, List nestingContext) + throws UnresolvableKeyException { + return key; + } + } + + void shutdown(@Observes ShutdownEvent event, Vertx vertx) { + cache.stopTimer(vertx); + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java index 5e80ddfb94b172..dedfe32bf11565 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java @@ -9,11 +9,8 @@ import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.PublicJsonWebKey; -import org.jose4j.jws.JsonWebSignature; -import org.jose4j.lang.InvalidAlgorithmException; import org.jose4j.lang.JoseException; -import io.quarkus.logging.Log; import io.quarkus.oidc.OIDCException; public class JsonWebKeySet { @@ -86,13 +83,8 @@ public Key getKeyWithS256Thumbprint(String x5tS256) { return keysWithS256Thumbprints.get(x5tS256); } - public Key getKeyWithoutKeyIdAndThumbprint(JsonWebSignature jws) { - try { - List keys = keysWithoutKeyIdAndThumbprint.get(jws.getKeyType()); - return keys == null || keys.size() != 1 ? null : keys.get(0); - } catch (InvalidAlgorithmException ex) { - Log.debug("Token 'alg'(algorithm) header value is invalid", ex); - return null; - } + public Key getKeyWithoutKeyIdAndThumbprint(String keyType) { + List keys = keysWithoutKeyIdAndThumbprint.get(keyType); + return keys == null || keys.size() != 1 ? null : keys.get(0); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/MemoryCache.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/MemoryCache.java new file mode 100644 index 00000000000000..dd8e4943ef029a --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/MemoryCache.java @@ -0,0 +1,135 @@ +package io.quarkus.oidc.runtime; + +import java.time.Duration; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import io.vertx.core.Handler; +import io.vertx.core.Vertx; + +public class MemoryCache { + private volatile Long timerId = null; + + private final Map> cacheMap = new ConcurrentHashMap<>(); + private AtomicInteger size = new AtomicInteger(); + private final Duration cacheTimeToLive; + private final int cacheSize; + + public MemoryCache(Vertx vertx, Optional cleanUpTimerInterval, + Duration cacheTimeToLive, int cacheSize) { + this.cacheTimeToLive = cacheTimeToLive; + this.cacheSize = cacheSize; + init(vertx, cleanUpTimerInterval); + } + + private void init(Vertx vertx, Optional cleanUpTimerInterval) { + if (cleanUpTimerInterval.isPresent()) { + timerId = vertx.setPeriodic(cleanUpTimerInterval.get().toMillis(), new Handler() { + @Override + public void handle(Long event) { + // Remove all the entries which have expired + removeInvalidEntries(); + } + }); + } + } + + public void add(String key, T result) { + if (cacheSize > 0) { + if (!prepareSpaceForNewCacheEntry()) { + clearCache(); + } + cacheMap.put(key, new CacheEntry(result)); + } + } + + public T remove(String key) { + CacheEntry entry = removeCacheEntry(key); + return entry == null ? null : entry.result; + } + + public T get(String key) { + CacheEntry entry = cacheMap.get(key); + return entry == null ? null : entry.result; + } + + public boolean containsKey(String key) { + return cacheMap.containsKey(key); + } + + private void removeInvalidEntries() { + long now = now(); + for (Iterator>> it = cacheMap.entrySet().iterator(); it.hasNext();) { + Map.Entry> next = it.next(); + if (next != null) { + if (isEntryExpired(next.getValue(), now)) { + try { + it.remove(); + size.decrementAndGet(); + } catch (IllegalStateException ex) { + // continue + } + } + } + } + } + + private boolean prepareSpaceForNewCacheEntry() { + int currentSize; + do { + currentSize = size.get(); + if (currentSize == cacheSize) { + return false; + } + } while (!size.compareAndSet(currentSize, currentSize + 1)); + return true; + } + + private CacheEntry removeCacheEntry(String token) { + CacheEntry entry = cacheMap.remove(token); + if (entry != null) { + size.decrementAndGet(); + } + return entry; + } + + private boolean isEntryExpired(CacheEntry entry, long now) { + return entry.createdTime + cacheTimeToLive.toMillis() < now; + } + + private static long now() { + return System.currentTimeMillis(); + } + + private static class CacheEntry { + volatile T result; + long createdTime = System.currentTimeMillis(); + + public CacheEntry(T result) { + this.result = result; + } + } + + public int getCacheSize() { + return cacheMap.size(); + } + + public void clearCache() { + cacheMap.clear(); + size.set(0); + } + + public void stopTimer(Vertx vertx) { + if (timerId != null && vertx.cancelTimer(timerId)) { + timerId = null; + } + } + + public boolean isTimerRunning() { + return timerId != null; + } + +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 26d053339d97ac..4cc968508c72f9 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -142,7 +142,7 @@ public Uni apply(UserInfo userInfo, Throwable t) { primaryTokenUni = verifySelfSignedTokenUni(resolvedContext, request.getToken().getToken()); } } else { - primaryTokenUni = verifyTokenUni(requestData, resolvedContext, request.getToken().getToken(), + primaryTokenUni = verifyTokenUni(requestData, resolvedContext, request.getToken(), isIdToken(request), null); } @@ -192,7 +192,7 @@ public Uni apply(TokenVerificationResult codeAccessToken, Thro } Uni tokenUni = verifyTokenUni(requestData, resolvedContext, - request.getToken().getToken(), + request.getToken(), false, userInfo); return tokenUni.onItemOrFailure() @@ -421,14 +421,15 @@ private Uni verifyCodeFlowAccessTokenUni(Map verifyTokenUni(Map requestData, TenantConfigContext resolvedContext, - String token, boolean enforceAudienceVerification, UserInfo userInfo) { + TokenCredential tokenCred, boolean enforceAudienceVerification, UserInfo userInfo) { + final String token = tokenCred.getToken(); if (OidcUtils.isOpaqueToken(token)) { if (!resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection) { LOG.debug("Token is opaque but the opaque token introspection is not allowed"); @@ -452,7 +453,7 @@ private Uni verifyTokenUni(Map requestD // Verify JWT token with the remote introspection LOG.debug("Starting the JWT token introspection"); return introspectTokenUni(resolvedContext, token, false); - } else { + } else if (resolvedContext.oidcConfig.jwks.resolveEarly) { // Verify JWT token with the local JWK keys with a possible remote introspection fallback final String nonce = (String) requestData.get(OidcConstants.NONCE); try { @@ -470,6 +471,10 @@ private Uni verifyTokenUni(Map requestD return Uni.createFrom().failure(t); } } + } else { + final String nonce = (String) requestData.get(OidcConstants.NONCE); + return resolveJwksAndVerifyTokenUni(resolvedContext, tokenCred, enforceAudienceVerification, + resolvedContext.oidcConfig.token.isSubjectRequired(), nonce); } } @@ -488,6 +493,15 @@ private Uni refreshJwksAndVerifyTokenUni(TenantConfigCo .recoverWithUni(f -> introspectTokenUni(resolvedContext, token, true)); } + private Uni resolveJwksAndVerifyTokenUni(TenantConfigContext resolvedContext, + TokenCredential tokenCred, + boolean enforceAudienceVerification, boolean subjectRequired, String nonce) { + return resolvedContext.provider + .getKeyResolverAndVerifyJwtToken(tokenCred, enforceAudienceVerification, subjectRequired, nonce) + .onFailure(f -> fallbackToIntrospectionIfNoMatchingKey(f, resolvedContext)) + .recoverWithUni(f -> introspectTokenUni(resolvedContext, tokenCred.getToken(), true)); + } + private static boolean fallbackToIntrospectionIfNoMatchingKey(Throwable f, TenantConfigContext resolvedContext) { if (!(f.getCause() instanceof UnresolvableKeyException)) { LOG.debug("Local JWT token verification has failed, skipping the token introspection"); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index 32bbc806e2d6c3..8d26b8aae936cb 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -27,8 +27,10 @@ import org.jose4j.jwx.HeaderParameterNames; import org.jose4j.jwx.JsonWebStructure; import org.jose4j.keys.resolvers.VerificationKeyResolver; +import org.jose4j.lang.InvalidAlgorithmException; import org.jose4j.lang.UnresolvableKeyException; +import io.quarkus.logging.Log; import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; @@ -38,6 +40,7 @@ import io.quarkus.oidc.UserInfo; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.credential.TokenCredential; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.util.KeyUtils; import io.smallrye.mutiny.Uni; @@ -64,6 +67,7 @@ public class OidcProvider implements Closeable { final OidcProviderClient client; final RefreshableVerificationKeyResolver asymmetricKeyResolver; + final DynamicVerificationKeyResolver keyResolverProvider; final OidcTenantConfig oidcConfig; final TokenCustomizer tokenCustomizer; final String issuer; @@ -83,7 +87,11 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.tokenCustomizer = tokenCustomizer; this.asymmetricKeyResolver = jwks == null ? null : new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval); - + if (client != null && oidcConfig != null && !oidcConfig.jwks.resolveEarly) { + this.keyResolverProvider = new DynamicVerificationKeyResolver(client, oidcConfig); + } else { + this.keyResolverProvider = null; + } this.issuer = checkIssuerProp(); this.audience = checkAudienceProp(); this.requiredClaims = checkRequiredClaimsProp(); @@ -96,6 +104,7 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD this.oidcConfig = oidcConfig; this.tokenCustomizer = TokenCustomizerFinder.find(oidcConfig); this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc); + this.keyResolverProvider = null; this.issuer = checkIssuerProp(); this.audience = checkAudienceProp(); this.requiredClaims = checkRequiredClaimsProp(); @@ -282,6 +291,30 @@ public Uni apply(Void v) { }); } + public Uni getKeyResolverAndVerifyJwtToken(TokenCredential tokenCred, + boolean enforceAudienceVerification, + boolean subjectRequired, String nonce) { + return keyResolverProvider.resolve(tokenCred).onItem() + .transformToUni(new Function>() { + + @Override + public Uni apply(VerificationKeyResolver resolver) { + try { + return Uni.createFrom() + .item(verifyJwtTokenInternal(customizeJwtToken(tokenCred.getToken()), + enforceAudienceVerification, + subjectRequired, nonce, + (requiredAlgorithmConstraints != null ? requiredAlgorithmConstraints + : ASYMMETRIC_ALGORITHM_CONSTRAINTS), + resolver, true)); + } catch (Throwable t) { + return Uni.createFrom().failure(t); + } + } + + }); + } + public Uni introspectToken(String token, boolean fallbackFromJwkMatch) { if (client.getMetadata().getIntrospectionUri() == null) { String errorMessage = String.format("Token issued to client %s " @@ -380,7 +413,7 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex // Try 'kid' first String kid = jws.getKeyIdHeaderValue(); if (kid != null) { - key = getKeyWithId(jws, kid); + key = getKeyWithId(kid); if (key == null) { // if `kid` was set then the key must exist throw new UnresolvableKeyException(String.format("JWK with kid '%s' is not available", kid)); @@ -389,31 +422,35 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex String thumbprint = null; if (key == null) { - thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT); + thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT); if (thumbprint != null) { - key = getKeyWithThumbprint(jws, thumbprint); + key = getKeyWithS256Thumbprint(thumbprint); if (key == null) { - // if only `x5t` was set then the key must exist + // if only `x5tS256` was set then the key must exist throw new UnresolvableKeyException( - String.format("JWK with the certificate thumbprint '%s' is not available", thumbprint)); + String.format("JWK with the SHA256 certificate thumbprint '%s' is not available", thumbprint)); } } } if (key == null) { - thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT); + thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT); if (thumbprint != null) { - key = getKeyWithS256Thumbprint(jws, thumbprint); + key = getKeyWithThumbprint(thumbprint); if (key == null) { - // if only `x5tS256` was set then the key must exist + // if only `x5t` was set then the key must exist throw new UnresolvableKeyException( - String.format("JWK with the SHA256 certificate thumbprint '%s' is not available", thumbprint)); + String.format("JWK with the certificate thumbprint '%s' is not available", thumbprint)); } } } if (key == null && kid == null && thumbprint == null) { - key = jwks.getKeyWithoutKeyIdAndThumbprint(jws); + try { + key = jwks.getKeyWithoutKeyIdAndThumbprint(jws.getKeyType()); + } catch (InvalidAlgorithmException ex) { + Log.debug("Token 'alg'(algorithm) header value is invalid", ex); + } } if (key == null) { @@ -425,7 +462,7 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex } } - private Key getKeyWithId(JsonWebSignature jws, String kid) { + private Key getKeyWithId(String kid) { if (kid != null) { return jwks.getKeyWithId(kid); } else { @@ -434,7 +471,7 @@ private Key getKeyWithId(JsonWebSignature jws, String kid) { } } - private Key getKeyWithThumbprint(JsonWebSignature jws, String thumbprint) { + private Key getKeyWithThumbprint(String thumbprint) { if (thumbprint != null) { return jwks.getKeyWithThumbprint(thumbprint); } else { @@ -443,7 +480,7 @@ private Key getKeyWithThumbprint(JsonWebSignature jws, String thumbprint) { } } - private Key getKeyWithS256Thumbprint(JsonWebSignature jws, String thumbprint) { + private Key getKeyWithS256Thumbprint(String thumbprint) { if (thumbprint != null) { return jwks.getKeyWithS256Thumbprint(thumbprint); } else { @@ -456,15 +493,16 @@ public Uni refresh() { final long now = now(); if (now > lastForcedRefreshTime + forcedJwksRefreshIntervalMilliSecs) { lastForcedRefreshTime = now; - return client.getJsonWebKeySet().onItem().transformToUni(new Function>() { + return client.getJsonWebKeySet(null).onItem() + .transformToUni(new Function>() { - @Override - public Uni apply(JsonWebKeySet t) { - jwks = t; - return Uni.createFrom().voidItem(); - } + @Override + public Uni apply(JsonWebKeySet t) { + jwks = t; + return Uni.createFrom().voidItem(); + } - }); + }); } else { return Uni.createFrom().voidItem(); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index ed4abe2aafecfd..204c38984259c3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -15,12 +15,14 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; +import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.common.runtime.OidcEndpointAccessException; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.groups.UniOnItem; +import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.core.json.JsonObject; import io.vertx.mutiny.core.MultiMap; @@ -40,6 +42,7 @@ public class OidcProviderClient implements Closeable { private static final String APPLICATION_JSON = "application/json"; private final WebClient client; + private final Vertx vertx; private final OidcConfigurationMetadata metadata; private final OidcTenantConfig oidcConfig; private final String clientSecretBasicAuthScheme; @@ -48,10 +51,12 @@ public class OidcProviderClient implements Closeable { private final List filters; public OidcProviderClient(WebClient client, + Vertx vertx, OidcConfigurationMetadata metadata, OidcTenantConfig oidcConfig, List filters) { this.client = client; + this.vertx = vertx; this.metadata = metadata; this.oidcConfig = oidcConfig; this.clientSecretBasicAuthScheme = OidcCommonUtils.initClientSecretBasicAuth(oidcConfig); @@ -74,14 +79,14 @@ public OidcConfigurationMetadata getMetadata() { return metadata; } - public Uni getJsonWebKeySet() { - return filter(client.getAbs(metadata.getJsonWebKeySetUri()), null).send().onItem() + public Uni getJsonWebKeySet(OidcRequestContextProperties contextProperties) { + return filter(client.getAbs(metadata.getJsonWebKeySetUri()), null, contextProperties).send().onItem() .transform(resp -> getJsonWebKeySet(resp)); } public Uni getUserInfo(String token) { LOG.debugf("Get UserInfo on: %s auth: %s", metadata.getUserInfoUri(), OidcConstants.BEARER_SCHEME + " " + token); - return filter(client.getAbs(metadata.getUserInfoUri()), null) + return filter(client.getAbs(metadata.getUserInfoUri()), null, null) .putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + token) .send().onItem().transform(resp -> getUserInfo(resp)); } @@ -163,7 +168,7 @@ private UniOnItem> getHttpResponse(String uri, MultiMap for LOG.debugf("Get token on: %s params: %s headers: %s", metadata.getTokenUri(), formBody, request.headers()); // Retry up to three times with a one-second delay between the retries if the connection is closed. Buffer buffer = OidcCommonUtils.encodeForm(formBody); - Uni> response = filter(request, buffer).sendBuffer(buffer) + Uni> response = filter(request, buffer, null).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount).onFailure().transform(t -> t.getCause()); @@ -219,10 +224,15 @@ public Key getClientJwtKey() { return clientJwtKey; } - private HttpRequest filter(HttpRequest request, Buffer body) { + private HttpRequest filter(HttpRequest request, Buffer body, + OidcRequestContextProperties contextProperties) { for (OidcRequestFilter filter : filters) { - filter.filter(request, body, null); + filter.filter(request, body, contextProperties); } return request; } + + public Vertx getVertx() { + return vertx; + } } 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 d169d2bcd20794..50264f617dfd54 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 @@ -352,17 +352,16 @@ protected static Uni createOidcProvider(OidcTenantConfig oidcConfi .transformToUni(new Function>() { @Override public Uni apply(OidcProviderClient client) { - if (client.getMetadata().getJsonWebKeySetUri() != null + if (oidcConfig.jwks.resolveEarly + && client.getMetadata().getJsonWebKeySetUri() != null && !oidcConfig.token.requireJwtIntrospectionOnly) { return getJsonWebSetUni(client, oidcConfig).onItem() .transform(new Function() { - @Override public OidcProvider apply(JsonWebKeySet jwks) { return new OidcProvider(client, oidcConfig, jwks, readTokenDecryptionKey(oidcConfig)); } - }); } else { return Uni.createFrom() @@ -405,7 +404,7 @@ private static Key readTokenDecryptionKey(OidcTenantConfig oidcConfig) { protected static Uni getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) { if (!oidcConfig.isDiscoveryEnabled().orElse(true)) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); - return client.getJsonWebKeySet().onFailure(OidcCommonUtils.oidcEndpointNotAvailable()) + return client.getJsonWebKeySet(null).onFailure(OidcCommonUtils.oidcEndpointNotAvailable()) .retry() .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, OidcCommonUtils.CONNECTION_BACKOFF_DURATION) .expireIn(connectionDelayInMillisecs) @@ -419,7 +418,7 @@ public Throwable apply(Throwable t) { .onFailure() .invoke(client::close); } else { - return client.getJsonWebKeySet(); + return client.getJsonWebKeySet(null); } } @@ -479,7 +478,7 @@ public Uni apply(OidcConfigurationMetadata metadata, Throwab + " Use 'quarkus.oidc.user-info-path' if the discovery is disabled.")); } return Uni.createFrom() - .item(new OidcProviderClient(client, metadata, oidcConfig, clientRequestFilters)); + .item(new OidcProviderClient(client, vertx, metadata, oidcConfig, clientRequestFilters)); } }); diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/MemoryCacheTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/MemoryCacheTest.java new file mode 100644 index 00000000000000..6e0d7f082f6c14 --- /dev/null +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/MemoryCacheTest.java @@ -0,0 +1,101 @@ +package io.quarkus.oidc.runtime; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Callable; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import io.vertx.core.Vertx; + +public class MemoryCacheTest { + + static Vertx vertx = Vertx.vertx(); + + @AfterAll + public static void closeVertxClient() { + if (vertx != null) { + vertx.close().toCompletionStage().toCompletableFuture().join(); + vertx = null; + } + } + + @Test + public void testCache() throws Exception { + + MemoryCache cache = new MemoryCache(vertx, + // timer interval + Optional.of(Duration.ofSeconds(1)), + // entry is valid for 3 seconds + Duration.ofSeconds(2), + // max cache size + 2); + cache.add("1", new Bean("1")); + cache.add("2", new Bean("2")); + assertEquals(2, cache.getCacheSize()); + + assertEquals("1", cache.get("1").name); + assertEquals("2", cache.get("2").name); + + assertEquals("1", cache.remove("1").name); + assertNull(cache.get("1")); + assertEquals("2", cache.get("2").name); + assertEquals(1, cache.getCacheSize()); + + assertTrue(cache.isTimerRunning()); + + await().atMost(Duration.ofSeconds(5)).until(new Callable() { + + @Override + public Boolean call() throws Exception { + return cache.getCacheSize() == 0; + } + + }); + + cache.stopTimer(vertx); + assertFalse(cache.isTimerRunning()); + } + + @Test + public void testAddWhenMaxCacheSizeIsReached() throws Exception { + + MemoryCache cache = new MemoryCache(vertx, + // timer interval + Optional.empty(), + // entry is valid for 3 seconds + Duration.ofSeconds(3), + // max cache size + 2); + assertFalse(cache.isTimerRunning()); + + cache.add("1", new Bean("1")); + cache.add("2", new Bean("2")); + assertEquals(2, cache.getCacheSize()); + + // Currently, if the cache is full and a new entry has to be added, then the whole cache is cleared + // It can be optimized to remove the oldest entry only in the future + + cache.add("3", new Bean("3")); + assertEquals(1, cache.getCacheSize()); + + assertNull(cache.get("1")); + assertNull(cache.get("2")); + assertEquals("3", cache.get("3").name); + } + + static class Bean { + String name; + + Bean(String name) { + this.name = name; + } + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java index b26cb0aa8049cb..0f76995ecd0ed4 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java @@ -3,6 +3,7 @@ import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.core.http.HttpMethod; @@ -18,7 +19,22 @@ public void filter(HttpRequest request, Buffer buffer, OidcRequestContex HttpMethod method = request.method(); String uri = request.uri(); if (method == HttpMethod.GET && uri.endsWith("/auth/azure/jwk")) { - request.putHeader("Authorization", "ID token"); + String token = contextProps.getString(OidcRequestContextProperties.TOKEN); + AccessTokenCredential tokenCred = contextProps.get(OidcRequestContextProperties.TOKEN_CREDENTIAL, + AccessTokenCredential.class); + // or + // IdTokenCredential tokenCred = contextProps.get(OidcRequestContextProperties.TOKEN_CREDENTIAL, + // IdTokenCredential.class); + // or + // TokenCredential tokenCred = contextProps.get(OidcRequestContextProperties.TOKEN_CREDENTIAL, + // TokenCredential.class); + // if either access or ID token has to be verified and check is it an instanceof + // AccessTokenCredential or IdTokenCredential + // or simply + // String token = contextProps.getString(OidcRequestContextProperties.TOKEN); + if (token.equals(tokenCred.getToken())) { + request.putHeader("Authorization", "Access token: " + token); + } } } diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index f806a8948240c2..1a0e9556492dee 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -130,6 +130,7 @@ quarkus.oidc.bearer-azure.provider=microsoft quarkus.oidc.bearer-azure.application-type=service quarkus.oidc.bearer-azure.discovery-enabled=false quarkus.oidc.bearer-azure.jwks-path=${keycloak.url}/azure/jwk +quarkus.oidc.bearer-azure.jwks.resolve-early=false quarkus.oidc.bearer-azure.token.lifespan-grace=2147483647 quarkus.oidc.bearer-azure.token.customizer-name=azure-access-token-customizer diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index 4c0b332ce82ad2..4e31443081776b 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -50,11 +50,11 @@ public void testSecureAccessSuccessPreferredUsername() { @Test public void testAccessResourceAzure() throws Exception { + String azureToken = readFile("token.txt"); String azureJwk = readFile("jwks.json"); wireMockServer.stubFor(WireMock.get("/auth/azure/jwk") - .withHeader("Authorization", matching("ID token")) + .withHeader("Authorization", matching("Access token: " + azureToken)) .willReturn(WireMock.aResponse().withBody(azureJwk))); - String azureToken = readFile("token.txt"); RestAssured.given().auth().oauth2(azureToken) .when().get("/api/admin/bearer-azure") .then()