From 47110c6737c597c2e00f090fd5da269231f7769b Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 17 Nov 2023 11:51:21 +0100 Subject: [PATCH 01/24] Ignore files coming from quarkus-ide-launcher jar (cherry picked from commit 6d853647c015a4a64addbb4dcfdbf3f83bebdb16) --- independent-projects/parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/independent-projects/parent/pom.xml b/independent-projects/parent/pom.xml index 67ff724afd4c0..0c88d626eeb8e 100644 --- a/independent-projects/parent/pom.xml +++ b/independent-projects/parent/pom.xml @@ -422,7 +422,7 @@ - **/quarkus-ide-launcher-*.jar + META-INF/ide-deps/** From 5ea58a121a5b68de4081aa978331e79cbc27bce1 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Sat, 18 Nov 2023 13:43:50 +0100 Subject: [PATCH 02/24] Make sure we always include the Develocity scan URL in reports (cherry picked from commit 1af1783b1bf5fdbeae2a439138e2324841ffd187) --- .github/workflows/ci-actions-incremental.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 33e0b6c4b65c1..92b758f0d09ef 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -732,6 +732,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + **/target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 - name: Save Build Scan @@ -926,7 +927,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json - target/gradle-build-scan-url.txt + **/target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 - name: Save Build Scan @@ -1023,7 +1024,7 @@ jobs: **/target/*-reports/TEST-*.xml **/build/test-results/test/TEST-*.xml target/build-report.json - target/gradle-build-scan-url.txt + **/target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 - name: Save Build Scan From 3bc7085ffb42aa5a55ee8d6c73b1dae6c8a7629d Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Sat, 18 Nov 2023 13:50:46 +0100 Subject: [PATCH 03/24] Output JSON for debugging purposes (cherry picked from commit b45817a7c6c48097fbbb3ff7717524bb7318181a) --- .../develocity-publish-build-scans.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml index a285e66a8445b..40866c25e6f2e 100644 --- a/.github/workflows/develocity-publish-build-scans.yml +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -31,13 +31,12 @@ jobs: develocity-url: 'https://ge.quarkus.io' 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) }} + - name: Inject build scans in reports + uses: quarkusio/action-helpers@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + action: inject-build-scans + workflow-run-id: ${{ github.event.workflow_run.id }} + - name: Output JSON file 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} + if [ -f build-metadata.json ]; then jq '.' build-metadata.json >> $GITHUB_STEP_SUMMARY; fi From 5aa9cd3aacbed058cb2308979136dc56074130b9 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 19 Nov 2023 01:34:15 +0900 Subject: [PATCH 04/24] Fixed sample code for KotlinModule initialization. Initialization using the constructor has been deprecated. (cherry picked from commit 2948027551663dd735cd5d8f9b5ddf004db58c16) --- docs/src/main/asciidoc/kotlin.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/main/asciidoc/kotlin.adoc b/docs/src/main/asciidoc/kotlin.adoc index ef117f0c8d0fb..0d7bc89658eb4 100644 --- a/docs/src/main/asciidoc/kotlin.adoc +++ b/docs/src/main/asciidoc/kotlin.adoc @@ -470,9 +470,9 @@ import io.fabric8.kubernetes.client.utils.Serialization import com.fasterxml.jackson.module.kotlin.KotlinModule ... - -Serialization.jsonMapper().registerModule(KotlinModule()) -Serialization.yamlMapper().registerModule(KotlinModule()) +val kotlinModule = KotlinModule.Builder().build() +Serialization.jsonMapper().registerModule(kotlinModule) +Serialization.yamlMapper().registerModule(kotlinModule) ---- _Please test this carefully on compilation to native images and fallback to Java-compatible Jackson bindings if you experience problems._ From 4c2b0e0731824e82db333e188328ac5621ab9b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sun, 19 Nov 2023 17:28:15 +0100 Subject: [PATCH 05/24] Support SecureField roles allowed config expressions (cherry picked from commit c3bb17331acd5ecd8bf54ba0fb679959d98bcd49) --- docs/src/main/asciidoc/resteasy-reactive.adoc | 21 ++++++- .../deployment/pom.xml | 6 +- .../ResteasyReactiveJacksonProcessor.java | 62 +++++++++++++++++++ .../test/AbstractPersonResource.java | 2 + .../test/CustomSerializationResource.java | 2 + .../jackson/deployment/test/Person.java | 22 +++++++ .../deployment/test/SimpleJsonResource.java | 8 +++ .../deployment/test/SimpleJsonTest.java | 58 ++++++++++++++++- ...ResteasyReactiveServerJacksonRecorder.java | 45 ++++++++++++++ .../RolesAllowedConfigExpStorage.java | 34 ++++++++++ .../security/SecurityPropertyFilter.java | 31 ++++++++++ .../deployment/SecurityProcessor.java | 24 +++++-- .../runtime/SecurityCheckRecorder.java | 11 ++++ ...olesAllowedConfigExpResolverBuildItem.java | 43 +++++++++++++ 14 files changed, 361 insertions(+), 8 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/RolesAllowedConfigExpStorage.java create mode 100644 extensions/security/spi/src/main/java/io/quarkus/security/spi/RolesAllowedConfigExpResolverBuildItem.java diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 7d9792a76c145..a5e4e544f6649 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1301,6 +1301,8 @@ public class Person { private final Long id; private final String first; private final String last; + @SecureField(rolesAllowed = ${role:admin}") <1> + private String address; public Person(Long id, String first, String last) { this.id = id; @@ -1319,8 +1321,20 @@ public class Person { public String getLast() { return last; } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } } ---- +<1> The `io.quarkus.resteasy.reactive.jackson.SecureField.rolesAllowed` property supports xref:config-reference.adoc#property-expressions[property expressions] +exactly in the same fashion the `jakarta.annotation.security.RolesAllowed` annotation does. For more information, please +refer to the xref:security-authorize-web-endpoints-reference.adoc#standard-security-annotations[Standard security annotations] +section of the Authorization of web endpoints guide. A very simple Jakarta REST Resource that uses `Person` could be: @@ -1337,7 +1351,7 @@ public class Person { @Path("{id}") @GET public Person getPerson(Long id) { - return new Person(id, "foo", "bar"); + return new Person(id, "foo", "bar", "Brick Lane"); } } ---- @@ -1350,7 +1364,8 @@ performs an HTTP GET on `/person/1` they will receive: { "id": 1, "first": "foo", - "last": "bar" + "last": "bar", + "address", "Brick Lane" } ---- @@ -1369,6 +1384,8 @@ Any user however that does not have the `admin` role will receive: NOTE: No additional configuration needs to be applied for this secure serialization to take place. However, users can use the `@io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization` and `@io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization` annotation to opt in or out for specific Jakarta REST Resource classes or methods. +WARNING: Configuration expressions set with the `SecureField.rolesAllowed` property are validated during application startup even when the `@io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization` annotation is used. + ===== @JsonView support Jakarta REST methods can be annotated with https://fasterxml.github.io/jackson-annotations/javadoc/2.10/com/fasterxml/jackson/annotation/JsonView.html[@JsonView] diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/pom.xml index 87da39c8b69fc..f357b700c7e4c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/pom.xml @@ -50,7 +50,11 @@ quarkus-jaxrs-client-reactive-deployment test - + + io.quarkus + quarkus-security-test-utils + test + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index 74f4cc444bc45..f40508ca11665 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.jackson.deployment.processor; +import static io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem.isSecurityConfigExpressionCandidate; import static org.jboss.resteasy.reactive.common.util.RestMediaType.APPLICATION_NDJSON; import static org.jboss.resteasy.reactive.common.util.RestMediaType.APPLICATION_STREAM_JSON; @@ -10,7 +11,10 @@ import java.util.Locale; import java.util.Optional; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import jakarta.inject.Singleton; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.RuntimeType; import jakarta.ws.rs.core.Cookie; @@ -34,13 +38,20 @@ import com.fasterxml.jackson.databind.exc.MismatchedInputException; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SynthesisFinishedBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem; @@ -54,6 +65,7 @@ import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder; import io.quarkus.resteasy.reactive.jackson.runtime.mappers.DefaultMismatchedInputException; import io.quarkus.resteasy.reactive.jackson.runtime.mappers.NativeInvalidDefinitionExceptionMapper; +import io.quarkus.resteasy.reactive.jackson.runtime.security.RolesAllowedConfigExpStorage; import io.quarkus.resteasy.reactive.jackson.runtime.security.SecurityCustomSerialization; import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.BasicServerJacksonMessageBodyWriter; import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.FullyFeaturedServerJacksonMessageBodyReader; @@ -69,6 +81,7 @@ import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; +import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem; import io.quarkus.vertx.deployment.ReinitializeVertxJsonBuildItem; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -310,6 +323,46 @@ void handleJsonAnnotations(Optional resourceSca } } + @Record(ExecutionTime.STATIC_INIT) + @BuildStep + public void resolveRolesAllowedConfigExpressions(BuildProducer resolverProducer, + Capabilities capabilities, ResteasyReactiveServerJacksonRecorder recorder, CombinedIndexBuildItem indexBuildItem, + BuildProducer syntheticBeanProducer, + BuildProducer initAndValidateItemProducer) { + if (capabilities.isPresent(Capability.SECURITY)) { + BiConsumer> configValRecorder = null; + for (AnnotationInstance instance : indexBuildItem.getIndex().getAnnotations(SECURE_FIELD)) { + for (String role : instance.value("rolesAllowed").asStringArray()) { + if (isSecurityConfigExpressionCandidate(role)) { + if (configValRecorder == null) { + var storage = recorder.createConfigExpToAllowedRoles(); + configValRecorder = recorder.recordRolesAllowedConfigExpression(storage); + syntheticBeanProducer.produce(SyntheticBeanBuildItem + .configure(RolesAllowedConfigExpStorage.class) + .scope(Singleton.class) + .supplier(recorder.createRolesAllowedConfigExpStorage(storage)) + .unremovable() + .done()); + initAndValidateItemProducer.produce(new InitAndValidateRolesAllowedConfigExp()); + } + resolverProducer.produce(new RolesAllowedConfigExpResolverBuildItem(role, configValRecorder)); + } + } + } + } + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + @Consume(RuntimeConfigSetupCompleteBuildItem.class) + @Consume(SynthesisFinishedBuildItem.class) + public void initializeRolesAllowedConfigExp(ResteasyReactiveServerJacksonRecorder recorder, + Optional initAndValidateItem) { + if (initAndValidateItem.isPresent()) { + recorder.initAndValidateRolesAllowedConfigExp(); + } + } + @BuildStep public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntries, JaxRsResourceIndexBuildItem index, @@ -432,4 +485,13 @@ private String getMethodId(MethodInfo methodInfo, ClassInfo declaringClassInfo) return MethodId.get(methodInfo.name(), declaringClassInfo.name().toString(), parameterClassNames.toArray(EMPTY_STRING_ARRAY)); } + + /** + * Purely marker build item so that we know at least one allowed role with configuration + * expressions has been detected. + */ + public static final class InitAndValidateRolesAllowedConfigExp extends SimpleBuildItem { + private InitAndValidateRolesAllowedConfigExp() { + } + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPersonResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPersonResource.java index 00bb0ab2d3d51..5c9c00326443e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPersonResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPersonResource.java @@ -11,6 +11,8 @@ public Person abstractPerson() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); return person; } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializationResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializationResource.java index 6bbb582914d7a..becd3f13bed80 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializationResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializationResource.java @@ -44,6 +44,8 @@ public Person getPerson() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); return person; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Person.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Person.java index 7f5abadd4cc69..133e3a9f619e4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Person.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Person.java @@ -17,6 +17,12 @@ public class Person { @JsonView(Views.Private.class) public int id = 0; + @SecureField(rolesAllowed = { "${admin-expression:disabled}", "${user-expression:disabled}" }) + private String address; + + @SecureField(rolesAllowed = "${birth-date-roles:disabled}") + private String birthDate; + public String getFirst() { return first; } @@ -40,4 +46,20 @@ public int getId() { public void setId(int id) { this.id = id; } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getBirthDate() { + return birthDate; + } + + public void setBirthDate(String birthDate) { + this.birthDate = birthDate; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index b538997b62f25..9621a6fdde84a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -56,6 +56,8 @@ public Person getPerson() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); return person; } @@ -247,6 +249,8 @@ public void run() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); response.resume(person); } }).start(); @@ -285,6 +289,8 @@ public Multi getMulti1() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); return Multi.createFrom().items(person); } @@ -294,6 +300,8 @@ public Multi getMulti2() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); Person person2 = new Person(); person2.setFirst("Bob2"); person2.setLast("Builder2"); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index 9ebb1b30d6d5e..a36cd7a2aef68 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -11,10 +11,13 @@ 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.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; @@ -28,7 +31,10 @@ public JavaArchive get() { return ShrinkWrap.create(JavaArchive.class) .addClasses(Person.class, SimpleJsonResource.class, User.class, Views.class, SuperClass.class, OtherPersonResource.class, AbstractPersonResource.class, DataItem.class, Item.class, - NoopReaderInterceptor.class); + NoopReaderInterceptor.class, TestIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset("admin-expression=admin\n" + + "user-expression=user\n" + + "birth-date-roles=alice,bob\n"), "application.properties"); } }); @@ -389,6 +395,52 @@ public void testSecureRestResponsePerson() { doTestSecurePerson("/simple", "/secure-rest-response-person"); } + @Test + public void testSecureFieldRolesAllowedConfigExp() { + TestIdentityController.resetRoles().add("max", "max", "admin"); + RestAssured.given() + .auth().preemptive().basic("max", "max") + .get("/simple/secure-person") + .then() + .statusCode(200) + .contentType("application/json") + .header("transfer-encoding", nullValue()) + .header("content-length", notNullValue()) + .body(containsString("Bob")) + .body(containsString("0")) + .body(containsString("10 Downing St")) + .body(not(containsString("November 30, 1874"))) + .body(containsString("Builder")); + TestIdentityController.resetRoles().add("max", "max", "user"); + RestAssured.given() + .auth().preemptive().basic("max", "max") + .get("/simple/secure-person") + .then() + .statusCode(200) + .contentType("application/json") + .header("transfer-encoding", nullValue()) + .header("content-length", notNullValue()) + .body(containsString("Bob")) + .body(containsString("0")) + .body(containsString("10 Downing St")) + .body(not(containsString("November 30, 1874"))) + .body(not(containsString("Builder"))); + TestIdentityController.resetRoles().add("max", "max", "alice"); + RestAssured.given() + .auth().preemptive().basic("max", "max") + .get("/simple/secure-person") + .then() + .statusCode(200) + .contentType("application/json") + .header("transfer-encoding", nullValue()) + .header("content-length", notNullValue()) + .body(containsString("Bob")) + .body(containsString("0")) + .body(not(containsString("10 Downing St"))) + .body(containsString("November 30, 1874")) + .body(not(containsString("Builder"))); + } + private void doTestSecurePerson(String basePath, final String path) { RestAssured.get(basePath + path) .then() @@ -398,6 +450,8 @@ private void doTestSecurePerson(String basePath, final String path) { .header("content-length", notNullValue()) .body(containsString("Bob")) .body(containsString("0")) + .body(not(containsString("10 Downing St"))) + .body(not(containsString("November 30, 1874"))) .body(not(containsString("Builder"))); } @@ -410,6 +464,8 @@ private void doTestSecurePersonWithPublicView(String basePath, final String path .header("content-length", notNullValue()) .body(containsString("Bob")) .body(not(containsString("0"))) + .body(not(containsString("10 Downing St"))) + .body(not(containsString("November 30, 1874"))) .body(not(containsString("Builder"))); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java index 2e86508b9f498..a55ddadc4e3ce 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java @@ -3,12 +3,18 @@ import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Supplier; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; +import io.quarkus.arc.Arc; +import io.quarkus.resteasy.reactive.jackson.runtime.security.RolesAllowedConfigExpStorage; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; @@ -19,6 +25,45 @@ public class ResteasyReactiveServerJacksonRecorder { private static final Map> customSerializationMap = new HashMap<>(); private static final Map> customDeserializationMap = new HashMap<>(); + /* STATIC INIT */ + public RuntimeValue>> createConfigExpToAllowedRoles() { + return new RuntimeValue<>(new ConcurrentHashMap<>()); + } + + /* STATIC INIT */ + public BiConsumer> recordRolesAllowedConfigExpression( + RuntimeValue>> configExpToAllowedRoles) { + return new BiConsumer>() { + @Override + public void accept(String configKey, Supplier configValueSupplier) { + configExpToAllowedRoles.getValue().put(configKey, configValueSupplier); + } + }; + } + + /* STATIC INIT */ + public Supplier createRolesAllowedConfigExpStorage( + RuntimeValue>> configExpToAllowedRoles) { + return new Supplier() { + @Override + public RolesAllowedConfigExpStorage get() { + Map> map = configExpToAllowedRoles.getValue(); + if (map.isEmpty()) { + // there is no reason why this should happen, because we initialize the bean ourselves + // when runtime configuration is ready + throw new IllegalStateException( + "The 'RolesAllowedConfigExpStorage' bean is created before runtime configuration is ready"); + } + return new RolesAllowedConfigExpStorage(configExpToAllowedRoles.getValue()); + } + }; + } + + /* RUNTIME INIT */ + public void initAndValidateRolesAllowedConfigExp() { + Arc.container().instance(RolesAllowedConfigExpStorage.class).get().resolveRolesAllowedConfigExp(); + } + public void recordJsonView(String targetId, String className) { jsonViewMap.put(targetId, loadClass(className)); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/RolesAllowedConfigExpStorage.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/RolesAllowedConfigExpStorage.java new file mode 100644 index 0000000000000..e1ef44af6bed1 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/RolesAllowedConfigExpStorage.java @@ -0,0 +1,34 @@ +package io.quarkus.resteasy.reactive.jackson.runtime.security; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class RolesAllowedConfigExpStorage { + + private final Map> configExpToAllowedRoles; + private final Map rolesAllowedExpCache; + + public RolesAllowedConfigExpStorage(Map> configExpToAllowedRoles) { + this.configExpToAllowedRoles = Map.copyOf(configExpToAllowedRoles); + this.rolesAllowedExpCache = new HashMap<>(); + } + + /** + * Transforms configuration expressions to configuration values. + * Should be called on startup once runtime config is ready. + */ + public synchronized void resolveRolesAllowedConfigExp() { + if (rolesAllowedExpCache.isEmpty()) { + for (Map.Entry> e : configExpToAllowedRoles.entrySet()) { + String roleConfigExp = e.getKey(); + Supplier rolesSupplier = e.getValue(); + rolesAllowedExpCache.put(roleConfigExp, rolesSupplier.get()); + } + } + } + + String[] getRoles(String configExpression) { + return rolesAllowedExpCache.get(configExpression); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/SecurityPropertyFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/SecurityPropertyFilter.java index e361614fb7f3b..7d08f8b0fce5c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/SecurityPropertyFilter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/SecurityPropertyFilter.java @@ -12,6 +12,23 @@ public class SecurityPropertyFilter extends SimpleBeanPropertyFilter { static final String FILTER_ID = "securityFilter"; + private volatile InstanceHandle rolesAllowedConfigExpStorage; + + private RolesAllowedConfigExpStorage getRolesAllowedConfigExpStorage(ArcContainer container) { + if (rolesAllowedConfigExpStorage == null) { + synchronized (this) { + if (rolesAllowedConfigExpStorage == null) { + rolesAllowedConfigExpStorage = container.instance(RolesAllowedConfigExpStorage.class); + } + } + } + + if (rolesAllowedConfigExpStorage.isAvailable()) { + return rolesAllowedConfigExpStorage.get(); + } else { + return null; + } + } @Override protected boolean include(PropertyWriter writer) { @@ -31,7 +48,21 @@ protected boolean include(PropertyWriter writer) { } SecurityIdentity securityIdentity = instance.get(); + RolesAllowedConfigExpStorage rolesConfigExpStorage = getRolesAllowedConfigExpStorage(container); for (String role : secureField.rolesAllowed()) { + if (rolesConfigExpStorage != null) { + // role config expression => resolved roles + String[] roles = rolesConfigExpStorage.getRoles(role); + if (roles != null) { + for (String r : roles) { + if (securityIdentity.hasRole(r)) { + return true; + } + } + continue; + } + // at this point, we know 'role' is not a configuration expression + } if (securityIdentity.hasRole(role)) { return true; } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index 6c4e61670bd02..d781085f835ac 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -105,6 +105,7 @@ import io.quarkus.security.runtime.interceptor.SecurityHandler; import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem; import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; +import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem; import io.quarkus.security.spi.runtime.AuthorizationController; import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController; import io.quarkus.security.spi.runtime.MethodDescription; @@ -520,6 +521,7 @@ void transformSecurityAnnotations(BuildProducer @Record(ExecutionTime.STATIC_INIT) void gatherSecurityChecks(BuildProducer syntheticBeans, BuildProducer configExpSecurityCheckProducer, + List rolesAllowedConfigExpResolverBuildItems, BeanArchiveIndexBuildItem beanArchiveBuildItem, BuildProducer classPredicate, BuildProducer configBuilderProducer, @@ -540,7 +542,7 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, IndexView index = beanArchiveBuildItem.getIndex(); Map securityChecks = gatherSecurityAnnotations(index, configExpSecurityCheckProducer, additionalSecured.values(), config.denyUnannotated, recorder, configBuilderProducer, - reflectiveClassBuildItemBuildProducer); + reflectiveClassBuildItemBuildProducer, rolesAllowedConfigExpResolverBuildItems); for (AdditionalSecurityCheckBuildItem additionalSecurityCheck : additionalSecurityChecks) { securityChecks.put(additionalSecurityCheck.getMethodInfo(), additionalSecurityCheck.getSecurityCheck()); @@ -587,7 +589,8 @@ private Map gatherSecurityAnnotations(IndexView index BuildProducer configExpSecurityCheckProducer, Collection additionalSecuredMethods, boolean denyUnannotated, SecurityCheckRecorder recorder, BuildProducer configBuilderProducer, - BuildProducer reflectiveClassBuildItemBuildProducer) { + BuildProducer reflectiveClassBuildItemBuildProducer, + List rolesAllowedConfigExpResolverBuildItems) { Map methodToInstanceCollector = new HashMap<>(); Map classAnnotations = new HashMap<>(); @@ -670,11 +673,24 @@ public SecurityCheck apply(Set allowedRolesSet) { })); } + final boolean registerRolesAllowedConfigSource; + // way to resolve roles allowed configuration expressions specified via annotations to configuration values + if (!rolesAllowedConfigExpResolverBuildItems.isEmpty()) { + registerRolesAllowedConfigSource = true; + for (RolesAllowedConfigExpResolverBuildItem item : rolesAllowedConfigExpResolverBuildItems) { + recorder.recordRolesAllowedConfigExpression(item.getRoleConfigExpr(), keyIndex.getAndIncrement(), + item.getConfigValueRecorder()); + } + } else { + registerRolesAllowedConfigSource = hasRolesAllowedCheckWithConfigExp.get(); + } + if (hasRolesAllowedCheckWithConfigExp.get()) { - // make sure config expressions are resolved when app starts + // make sure config expressions are eagerly resolved inside security checks when app starts configExpSecurityCheckProducer .produce(new ConfigExpRolesAllowedSecurityCheckBuildItem()); - + } + if (registerRolesAllowedConfigSource) { // register config source with the Config system configBuilderProducer .produce(new RunTimeConfigBuilderBuildItem(QuarkusSecurityRolesAllowedConfigBuilder.class.getName())); diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index 8ffc983e7e057..ee6639d2ef495 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -10,6 +10,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -68,6 +69,16 @@ public SecurityCheck rolesAllowedSupplier(String[] allowedRoles, int[] configExp return check; } + /* STATIC INIT */ + public void recordRolesAllowedConfigExpression(String configExpression, int configKeyIndex, + BiConsumer> configValueRecorder) { + QuarkusSecurityRolesAllowedConfigBuilder.addProperty(configKeyIndex, configExpression); + // one configuration expression resolves to string array because the expression can be list treated as list + Supplier configValSupplier = resolveRolesAllowedConfigExp(new String[] { configExpression }, + new int[] { 0 }, new int[] { configKeyIndex }); + configValueRecorder.accept(configExpression, configValSupplier); + } + private static Supplier resolveRolesAllowedConfigExp(String[] allowedRoles, int[] configExpIndexes, int[] configKeys) { diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/RolesAllowedConfigExpResolverBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/RolesAllowedConfigExpResolverBuildItem.java new file mode 100644 index 0000000000000..95649dd6ca6ed --- /dev/null +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/RolesAllowedConfigExpResolverBuildItem.java @@ -0,0 +1,43 @@ +package io.quarkus.security.spi; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Provides a way to transform roles allowed specified as configuration expressions in annotations to runtime + * configuration values. + */ +public final class RolesAllowedConfigExpResolverBuildItem extends MultiBuildItem { + private final String roleConfigExpr; + private final BiConsumer> configValueRecorder; + + /** + * @param roleConfigExpr roles allowed configuration expression + * @param configValueRecorder roles allowed supplier will be recorded to this consumer created during static-init; + * runtime roles allowed expressions are supplied correctly only when runtime config is ready + */ + public RolesAllowedConfigExpResolverBuildItem(String roleConfigExpr, + BiConsumer> configValueRecorder) { + this.roleConfigExpr = Objects.requireNonNull(roleConfigExpr); + this.configValueRecorder = Objects.requireNonNull(configValueRecorder); + } + + public String getRoleConfigExpr() { + return roleConfigExpr; + } + + public BiConsumer> getConfigValueRecorder() { + return configValueRecorder; + } + + public static boolean isSecurityConfigExpressionCandidate(String configExpression) { + if (configExpression == null || configExpression.length() < 4) { + return false; + } + final int exprStart = configExpression.indexOf("${"); + return exprStart >= 0 && configExpression.indexOf('}', exprStart + 2) > 0; + } +} From 572e22701235cf9617179fb91f2f9681d81d5f8b Mon Sep 17 00:00:00 2001 From: barreiro Date: Mon, 20 Nov 2023 01:12:39 +0000 Subject: [PATCH 06/24] recognize quarkus.tls.trust-all property by keycloak-admin-client extension (cherry picked from commit 6e414c29dcc77c8f168b233277a7989f78ab8b94) --- .../reactive/KeycloakAdminClientReactiveProcessor.java | 5 +++-- .../reactive/runtime/ResteasyReactiveClientProvider.java | 8 +++++++- .../ResteasyReactiveKeycloakAdminClientRecorder.java | 4 ++-- .../deployment/KeycloakAdminClientProcessor.java | 5 +++-- .../adminclient/ResteasyKeycloakAdminClientRecorder.java | 4 ++-- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java b/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java index 0a486c5e024d1..27c8d4a6f95c2 100644 --- a/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java +++ b/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java @@ -25,6 +25,7 @@ import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled; import io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyReactiveClientProvider; import io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyReactiveKeycloakAdminClientRecorder; +import io.quarkus.runtime.TlsConfig; public class KeycloakAdminClientReactiveProcessor { @@ -53,8 +54,8 @@ public void nativeImage(BuildProducer serviceProviderP @Record(ExecutionTime.STATIC_INIT) @Produce(ServiceStartBuildItem.class) @BuildStep - public void integrate(ResteasyReactiveKeycloakAdminClientRecorder recorder) { - recorder.setClientProvider(); + public void integrate(ResteasyReactiveKeycloakAdminClientRecorder recorder, TlsConfig tlsConfig) { + recorder.setClientProvider(tlsConfig.trustAll); } @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java index a8bb66a6d0096..c39ffee71d45a 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java @@ -30,9 +30,15 @@ public class ResteasyReactiveClientProvider implements ResteasyClientProvider { private static final List HANDLED_MEDIA_TYPES = List.of(MediaType.APPLICATION_JSON); private static final int PROVIDER_PRIORITY = Priorities.USER + 100; // ensures that it will be used first + private final boolean tlsTrustAll; + + public ResteasyReactiveClientProvider(boolean tlsTrustAll) { + this.tlsTrustAll = tlsTrustAll; + } + @Override public Client newRestEasyClient(Object messageHandler, SSLContext sslContext, boolean disableTrustManager) { - ClientBuilderImpl clientBuilder = new ClientBuilderImpl().trustAll(disableTrustManager); + ClientBuilderImpl clientBuilder = new ClientBuilderImpl().trustAll(tlsTrustAll || disableTrustManager); return registerJacksonProviders(clientBuilder).build(); } diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java index 61d7605485442..12458c795592f 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java @@ -21,8 +21,8 @@ public ResteasyReactiveKeycloakAdminClientRecorder( this.keycloakAdminClientConfigRuntimeValue = keycloakAdminClientConfigRuntimeValue; } - public void setClientProvider() { - Keycloak.setClientProvider(new ResteasyReactiveClientProvider()); + public void setClientProvider(boolean tlsTrustAll) { + Keycloak.setClientProvider(new ResteasyReactiveClientProvider(tlsTrustAll)); } public Supplier createAdminClient() { diff --git a/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java b/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java index 056b3c1d5bf93..5ac5f6fef3237 100644 --- a/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java +++ b/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java @@ -25,6 +25,7 @@ import io.quarkus.keycloak.admin.client.common.AutoCloseableDestroyer; import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled; import io.quarkus.keycloak.adminclient.ResteasyKeycloakAdminClientRecorder; +import io.quarkus.runtime.TlsConfig; public class KeycloakAdminClientProcessor { @@ -48,8 +49,8 @@ ReflectiveClassBuildItem reflect() { @Record(ExecutionTime.STATIC_INIT) @Produce(ServiceStartBuildItem.class) @BuildStep - public void integrate(ResteasyKeycloakAdminClientRecorder recorder) { - recorder.setClientProvider(); + public void integrate(ResteasyKeycloakAdminClientRecorder recorder, TlsConfig tlsConfig) { + recorder.setClientProvider(tlsConfig.trustAll); } @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java index 9dda7e9c3c475..75fb6d2924896 100644 --- a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java @@ -58,13 +58,13 @@ public Keycloak get() { }; } - public void setClientProvider() { + public void setClientProvider(boolean tlsTrustAll) { Keycloak.setClientProvider(new ResteasyClientClassicProvider() { @Override public Client newRestEasyClient(Object customJacksonProvider, SSLContext sslContext, boolean disableTrustManager) { // point here is to use default Quarkus providers rather than org.keycloak.admin.client.JacksonProvider // as it doesn't work properly in native mode - return ClientBuilderWrapper.create(sslContext, disableTrustManager).build(); + return ClientBuilderWrapper.create(sslContext, tlsTrustAll || disableTrustManager).build(); } }); } From 7b09488763a9ab6e8fafac20eee27224f21c44ef Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 20 Nov 2023 10:48:45 +0100 Subject: [PATCH 07/24] Build cache - Ignore more files for Spotless (cherry picked from commit 2712330280c6870a991f6b724cf4433ae7baaae7) --- independent-projects/parent/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/independent-projects/parent/pom.xml b/independent-projects/parent/pom.xml index 0c88d626eeb8e..be3075bf1548b 100644 --- a/independent-projects/parent/pom.xml +++ b/independent-projects/parent/pom.xml @@ -562,6 +562,8 @@ .settings/* target/* .cache/* + .factorypath + *.log true From 67e3a31be0e86ef1670b55ef585463d8e4c270c6 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 20 Nov 2023 10:49:00 +0100 Subject: [PATCH 08/24] Build cache - Avoid runtimeClasspath being overridden (cherry picked from commit 7824826929ab4bcecbcd55991b27a70988e41c81) --- integration-tests/oidc-client-reactive/pom.xml | 3 +++ integration-tests/rest-client-reactive/pom.xml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/integration-tests/oidc-client-reactive/pom.xml b/integration-tests/oidc-client-reactive/pom.xml index 5c9c5f1f2e838..5d504c6944136 100644 --- a/integration-tests/oidc-client-reactive/pom.xml +++ b/integration-tests/oidc-client-reactive/pom.xml @@ -156,6 +156,9 @@ + + META-INF/ide-deps/** + application.properties diff --git a/integration-tests/rest-client-reactive/pom.xml b/integration-tests/rest-client-reactive/pom.xml index 05fc6fb2cf4c5..e70e0e07b177d 100644 --- a/integration-tests/rest-client-reactive/pom.xml +++ b/integration-tests/rest-client-reactive/pom.xml @@ -156,6 +156,9 @@ + + META-INF/ide-deps/** + application.properties From fef4a1328c9a93b040b89958cd67454900b86abe Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 20 Nov 2023 11:04:46 +0100 Subject: [PATCH 09/24] Build cache - Avoid hardcoded path in generated test file (cherry picked from commit 5727c2dcbd1afc6c0e0a13bd7edcd3cb0085e204) --- .../project-using-test-template-from-extension/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml index 2750c013fadbf..eef420dac8589 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml @@ -46,7 +46,7 @@ io.quarkus integration-test-extension-that-defines-junit-test-extensions ${quarkus.version} - test + test @@ -109,7 +109,7 @@ - ${project.build.directory}/${project.build.finalName}-runner + \${project.build.directory}/\${project.build.finalName}-runner org.jboss.logmanager.LogManager \${maven.home} From 402aec19938ebee3aca88992bcf00a3408b7f9cf Mon Sep 17 00:00:00 2001 From: Bernhard Schuhmann Date: Mon, 20 Nov 2023 12:56:02 +0100 Subject: [PATCH 10/24] Use LinkedHashMap for parts map to ensure user input order (cherry picked from commit 6c69e6ddad8ab45bb4f5ec3419d7f1cbdcb41728) --- .../reactive/server/multipart/MultipartFormDataOutput.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java index df175a86839c8..3b8212907a3ca 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java @@ -1,7 +1,7 @@ package org.jboss.resteasy.reactive.server.multipart; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import jakarta.ws.rs.core.MediaType; @@ -10,7 +10,7 @@ * Used when a Resource method needs to return a multipart output */ public final class MultipartFormDataOutput { - private final Map parts = new HashMap<>(); + private final Map parts = new LinkedHashMap<>(); public Map getFormData() { return Collections.unmodifiableMap(parts); From ff7f9763afa2be9f3542428de96850e87c899167 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Thu, 16 Nov 2023 23:44:47 +0000 Subject: [PATCH 11/24] Support custom Authorization schemes for OIDC bearer tokens (cherry picked from commit 7a8d03522b31fcf6dbe926126cbf7a1c6ff37c89) --- .../io/quarkus/oidc/OidcTenantConfig.java | 14 ++++++ .../BearerAuthenticationMechanism.java | 17 +++++--- .../runtime/OidcAuthenticationMechanism.java | 5 +-- .../it/keycloak/CustomTenantResolver.java | 3 ++ .../io/quarkus/it/keycloak/UsersResource.java | 8 ++++ .../src/main/resources/application.properties | 6 +++ .../BearerTokenAuthorizationTest.java | 43 +++++++++++++++++++ 7 files changed, 87 insertions(+), 9 deletions(-) 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 ccee7c9ca76c5..731715560132d 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 @@ -1477,6 +1477,12 @@ public static Token fromAudience(String... audience) { @ConfigItem public Optional header = Optional.empty(); + /** + * HTTP Authorization header scheme. + */ + @ConfigItem(defaultValue = OidcConstants.BEARER_SCHEME) + public String authorizationScheme = OidcConstants.BEARER_SCHEME; + /** * Required signature algorithm. * OIDC providers support many signature algorithms but if necessary you can restrict @@ -1697,6 +1703,14 @@ public boolean isSubjectRequired() { public void setSubjectRequired(boolean subjectRequired) { this.subjectRequired = subjectRequired; } + + public String getAuthorizationScheme() { + return authorizationScheme; + } + + public void setAuthorizationScheme(String authorizationScheme) { + this.authorizationScheme = authorizationScheme; + } } public static enum ApplicationType { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java index 78f4f2fe978de..f6c22753ab98e 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java @@ -1,10 +1,11 @@ package io.quarkus.oidc.runtime; +import java.util.function.Function; + import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.OidcTenantConfig; -import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.security.ChallengeData; @@ -15,9 +16,6 @@ public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMechanism { - protected static final ChallengeData UNAUTHORIZED_CHALLENGE = new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), - HttpHeaderNames.WWW_AUTHENTICATE, OidcConstants.BEARER_SCHEME); - public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager, OidcTenantConfig oidcTenantConfig) { String token = extractBearerToken(context, oidcTenantConfig); @@ -29,7 +27,14 @@ public Uni authenticate(RoutingContext context, } public Uni getChallenge(RoutingContext context) { - return Uni.createFrom().item(UNAUTHORIZED_CHALLENGE); + Uni tenantContext = resolver.resolveContext(context); + return tenantContext.onItem().transformToUni(new Function>() { + @Override + public Uni apply(TenantConfigContext tenantContext) { + return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), + HttpHeaderNames.WWW_AUTHENTICATE, tenantContext.oidcConfig.token.authorizationScheme)); + } + }); } private String extractBearerToken(RoutingContext context, OidcTenantConfig oidcConfig) { @@ -49,7 +54,7 @@ private String extractBearerToken(RoutingContext context, OidcTenantConfig oidcC return headerValue; } - if (!OidcConstants.BEARER_SCHEME.equalsIgnoreCase(scheme)) { + if (!oidcConfig.token.authorizationScheme.equalsIgnoreCase(scheme)) { return null; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java index edf944566678b..bd5c8ab18aeea 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java @@ -23,8 +23,6 @@ @ApplicationScoped public class OidcAuthenticationMechanism implements HttpAuthenticationMechanism { - private static HttpCredentialTransport OIDC_SERVICE_TRANSPORT = new HttpCredentialTransport( - HttpCredentialTransport.Type.AUTHORIZATION, OidcConstants.BEARER_SCHEME); private static HttpCredentialTransport OIDC_WEB_APP_TRANSPORT = new HttpCredentialTransport( HttpCredentialTransport.Type.AUTHORIZATION_CODE, OidcConstants.CODE_FLOW_CODE); @@ -105,7 +103,8 @@ public HttpCredentialTransport apply(OidcTenantConfig oidcTenantConfig) { return null; } return isWebApp(context, oidcTenantConfig) ? OIDC_WEB_APP_TRANSPORT - : OIDC_SERVICE_TRANSPORT; + : new HttpCredentialTransport( + HttpCredentialTransport.Type.AUTHORIZATION, oidcTenantConfig.token.authorizationScheme); } }); } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 1df7467d064ad..34ffd429732e1 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -44,6 +44,9 @@ public String resolve(RoutingContext context) { if (path.endsWith("bearer")) { return "bearer"; } + if (path.endsWith("bearer-id")) { + return "bearer-id"; + } if (path.endsWith("bearer-required-algorithm")) { return "bearer-required-algorithm"; } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/UsersResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/UsersResource.java index 3e55f570cfbe9..37f4565cbca0b 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/UsersResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/UsersResource.java @@ -29,6 +29,14 @@ public User principalName() { return new User(identity.getPrincipal().getName()); } + @GET + @Path("/me/bearer-id") + @RolesAllowed("user") + @Produces(MediaType.APPLICATION_JSON) + public User principalNameId() { + return new User(identity.getPrincipal().getName()); + } + @GET @Path("/preferredUserName/bearer") @RolesAllowed("user") diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 1a0e9556492de..6ac2dbb4c4537 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -121,6 +121,12 @@ quarkus.oidc.bearer.credentials.secret=secret quarkus.oidc.bearer.token.audience=https://service.example.com quarkus.oidc.bearer.allow-token-introspection-cache=false +quarkus.oidc.bearer-id.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.bearer-id.client-id=quarkus-app +quarkus.oidc.bearer-id.credentials.secret=secret +quarkus.oidc.bearer-id.allow-token-introspection-cache=false +quarkus.oidc.bearer-id.token.authorization-scheme=ID + quarkus.oidc.bearer-required-algorithm.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.bearer-required-algorithm.client-id=quarkus-app quarkus.oidc.bearer-required-algorithm.credentials.secret=secret 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 4e31443081776..5cf6f2cc4f5f8 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 @@ -262,6 +262,17 @@ public void testExpiredBearerToken() { .header("WWW-Authenticate", equalTo("Bearer")); } + @Test + public void testBearerToken() { + String token = getAccessToken("alice", Set.of("user")); + + RestAssured.given().auth().oauth2(token).when() + .get("/api/users/me/bearer") + .then() + .statusCode(200) + .body(Matchers.containsString("alice")); + } + @Test public void testBearerTokenWrongIssuer() { String token = getAccessTokenWrongIssuer("alice", Set.of("user")); @@ -284,6 +295,38 @@ public void testBearerTokenWrongAudience() { .header("WWW-Authenticate", equalTo("Bearer")); } + @Test + public void testBearerTokenIdScheme() { + String token = getAccessToken("alice", Set.of("user")); + + RestAssured.given().header("Authorization", "ID " + token).when() + .get("/api/users/me/bearer-id") + .then() + .statusCode(200) + .body(Matchers.containsString("alice")); + } + + @Test + public void testBearerTokenIdSchemeButBearerSchemeIsUsed() { + String token = getAccessToken("alice", Set.of("user")); + + RestAssured.given().auth().oauth2(token).when() + .get("/api/users/me/bearer-id") + .then() + .statusCode(401); + } + + @Test + public void testBearerTokenIdSchemeWrongIssuer() { + String token = getAccessTokenWrongIssuer("alice", Set.of("user")); + + RestAssured.given().auth().oauth2(token).when() + .get("/api/users/me/bearer-id") + .then() + .statusCode(401) + .header("WWW-Authenticate", equalTo("ID")); + } + @Test public void testAcquiringIdentityOutsideOfHttpRequest() { String tenant = "bearer"; From d4eb2dc907b8aa57eede551228046f9df46167b7 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Mon, 20 Nov 2023 14:44:04 +0100 Subject: [PATCH 12/24] Update OpenJDK runtime image to 1.18 Update UBI minimal to 8.9 Update code to select OpenJDK Runtime image for Java 21 (cherry picked from commit 177edd51bfc523f0dc64fbff514f8920cd9f3be4) --- .../CompiledJavaVersionBuildItem.java | 13 ++++++++ .../main/asciidoc/building-native-image.adoc | 2 +- docs/src/main/asciidoc/container-image.adoc | 6 ++-- .../asciidoc/quarkus-runtime-base-image.adoc | 6 ++-- docs/src/main/asciidoc/virtual-threads.adoc | 4 +-- .../RedHatOpenJDKRuntimeBaseProviderTest.java | 14 +++++++-- .../UbiMinimalBaseProviderTest.java | 14 +++++++-- .../src/test/resources/openjdk-11-runtime | 2 +- .../src/test/resources/openjdk-17-runtime | 2 +- .../src/test/resources/openjdk-21-runtime | 18 +++++++++++ .../deployment/src/test/resources/ubi-java11 | 2 +- .../deployment/src/test/resources/ubi-java17 | 2 +- .../deployment/src/test/resources/ubi-java21 | 31 +++++++++++++++++++ .../deployment/ContainerImageJibConfig.java | 7 +++-- .../image/jib/deployment/JibProcessor.java | 9 ++++-- .../ContainerImageOpenshiftConfig.java | 23 ++++++++------ .../image/openshift/deployment/S2iConfig.java | 4 +-- .../deployment/ContainerImageS2iConfig.java | 12 ++++--- .../base/Dockerfile-layout.include.qute | 2 +- .../quarkus/tooling/dockerfiles/codestart.yml | 2 +- .../QuarkusCodestartGenerationTest.java | 8 ++--- .../awt/src/main/docker/Dockerfile.native | 2 +- .../src/main/docker/Dockerfile.jvm | 2 +- .../src/main/docker/Dockerfile.jvm | 2 +- .../src/main/docker/Dockerfile.jvm | 2 +- .../src/main/docker/Dockerfile.jvm | 2 +- .../src/main/docker/Dockerfile.jvm | 2 +- .../src/main/docker/Dockerfile.jvm | 2 +- .../src/main/docker/Dockerfile.jvm | 2 +- .../src/main/docker/Dockerfile.jvm | 2 +- 30 files changed, 149 insertions(+), 52 deletions(-) create mode 100644 extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime create mode 100644 extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java index 39c5385a9348e..88dadbcc63dd4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java @@ -30,6 +30,8 @@ public interface JavaVersion { Status isJava17OrHigher(); + Status isJava21OrHigher(); + Status isJava19OrHigher(); enum Status { @@ -58,6 +60,11 @@ public Status isJava17OrHigher() { return Status.UNKNOWN; } + @Override + public Status isJava21OrHigher() { + return Status.UNKNOWN; + } + @Override public Status isJava19OrHigher() { return Status.UNKNOWN; @@ -69,6 +76,7 @@ final class Known implements JavaVersion { private static final int JAVA_11_MAJOR = 55; private static final int JAVA_17_MAJOR = 61; private static final int JAVA_19_MAJOR = 63; + private static final int JAVA_21_MAJOR = 66; private final int determinedMajor; @@ -96,6 +104,11 @@ public Status isJava19OrHigher() { return higherOrEqualStatus(JAVA_19_MAJOR); } + @Override + public Status isJava21OrHigher() { + return higherOrEqualStatus(JAVA_21_MAJOR); + } + private Status higherOrEqualStatus(int javaMajor) { return determinedMajor >= javaMajor ? Status.TRUE : Status.FALSE; } diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index 96e4e1640cdb0..6e3712384afd5 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -532,7 +532,7 @@ The project generation has also provided a `Dockerfile.native` in the `src/main/ [source,dockerfile] ---- -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ diff --git a/docs/src/main/asciidoc/container-image.adoc b/docs/src/main/asciidoc/container-image.adoc index 953637e0a9159..eb5e7c3ebff19 100644 --- a/docs/src/main/asciidoc/container-image.adoc +++ b/docs/src/main/asciidoc/container-image.adoc @@ -47,7 +47,7 @@ For example, the presence of `src/main/jib/foo/bar` would result in `/foo/bar` There are cases where the built container image may need to have Java debugging conditionally enabled at runtime. -When the base image has not been changed (and therefore `ubi8/openjdk-11-runtime` or `ubi8/openjdk-17-runtime` is used), then the `quarkus.jib.jvm-arguments` configuration property can be used in order to +When the base image has not been changed (and therefore `ubi8/openjdk-11-runtime`, `ubi8/openjdk-17-runtime`, or `ubi8/openjdk-21-runtime` is used), then the `quarkus.jib.jvm-arguments` configuration property can be used in order to make the JVM listen on the debug port at startup. The exact configuration is: @@ -63,7 +63,7 @@ Other base images might provide launch scripts that enable debugging when an env The `quarkus.jib.jvm-entrypoint` configuration property can be used to completely override the container entry point and can thus be used to either hard code the JVM debug configuration or point to a script that handles the details. -For example, if the base images `ubi8/openjdk-11-runtime` or `ubi8/openjdk-17-runtime` are used to build the container, the entry point can be hard-coded on the application properties file. +For example, if the base images `ubi8/openjdk-11-runtime`, `ubi8/openjdk-17-runtime` or `ubi8/openjdk-21-runtime` are used to build the container, the entry point can be hard-coded on the application properties file. .Example application.properties [source,properties] @@ -88,7 +88,7 @@ java \ -jar quarkus-run.jar ---- -NOTE: `/home/jboss` is the WORKDIR for all quarkus binaries in the base images `ubi8/openjdk-11-runtime` and `ubi8/openjdk-17-runtime` (https://catalog.redhat.com/software/containers/ubi8/openjdk-17/618bdbf34ae3739687568813?container-tabs=dockerfile[Dockerfile for ubi8/openjdk-17-runtime, window="_blank"]) +NOTE: `/home/jboss` is the WORKDIR for all quarkus binaries in the base images `ubi8/openjdk-11-runtime`, `ubi8/openjdk-17-runtime` and `ubi8/openjdk-21-runtime` (https://catalog.redhat.com/software/containers/ubi8/openjdk-17/618bdbf34ae3739687568813?container-tabs=dockerfile[Dockerfile for ubi8/openjdk-17-runtime, window="_blank"]) ==== Multi-module projects and layering diff --git a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc index 9d94a898fc410..ba9f0ff6c8b9a 100644 --- a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc +++ b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc @@ -39,7 +39,7 @@ In this case, you need to use a multi-stage `dockerfile` to copy the required li [source, dockerfile] ---- # First stage - install the dependencies in an intermediate container -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 as BUILD +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 as BUILD RUN microdnf install freetype # Second stage - copy the dependencies @@ -62,7 +62,7 @@ If you need to have access to the full AWT support, you need more than just `lib [source, dockerfile] ---- # First stage - install the dependencies in an intermediate container -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 as BUILD +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 as BUILD RUN microdnf install freetype fontconfig # Second stage - copy the dependencies @@ -112,7 +112,7 @@ To use this base image, use the following `Dockerfile`: [source, dockerfile] ---- -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ diff --git a/docs/src/main/asciidoc/virtual-threads.adoc b/docs/src/main/asciidoc/virtual-threads.adoc index eba2cde157550..2ee3545979f74 100644 --- a/docs/src/main/asciidoc/virtual-threads.adoc +++ b/docs/src/main/asciidoc/virtual-threads.adoc @@ -349,10 +349,10 @@ To containerize your Quarkus application that use `@RunOnVirtualThread`, add the quarkus.container-image.build=true quarkus.container-image.group= quarkus.container-image.name= -quarkus.jib.base-jvm-image=eclipse-temurin:21-ubi9-minimal <1> +quarkus.jib.base-jvm-image=registry.access.redhat.com/ubi8/openjdk-21-runtime <1> quarkus.jib.platforms=linux/amd64,linux/arm64 <2> ---- -<1> Make sure you use a base image supporting virtual threads. Here we use an image providing Java 21. +<1> Make sure you use a base image supporting virtual threads. Here we use an image providing Java 21. Quarkus picks an image providing Java 21+ automatically if you do not set one. <2> Select the target architecture. You can select more than one to build multi-archs images. Then, build your container as you would do usually. diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java index 88afd20df90e4..6114615670116 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -16,7 +16,7 @@ void testImageWithJava11() { Path path = getPath("openjdk-11-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-11-runtime:1.17"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18"); assertThat(v.getJavaVersion()).isEqualTo(11); }); } @@ -26,11 +26,21 @@ void testImageWithJava17() { Path path = getPath("openjdk-17-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18"); assertThat(v.getJavaVersion()).isEqualTo(17); }); } + @Test + void testImageWithJava21() { + Path path = getPath("openjdk-21-runtime"); + var result = sut.determine(path); + assertThat(result).hasValueSatisfying(v -> { + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18"); + assertThat(v.getJavaVersion()).isEqualTo(21); + }); + } + @Test void testUnhandled() { Path path = getPath("ubi-java11"); diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java index de17e9675c3ca..29c266279ac38 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java @@ -16,7 +16,7 @@ void testImageWithJava11() { Path path = getPath("ubi-java11"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.8"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); assertThat(v.getJavaVersion()).isEqualTo(11); }); } @@ -26,11 +26,21 @@ void testImageWithJava17() { Path path = getPath("ubi-java17"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); assertThat(v.getJavaVersion()).isEqualTo(17); }); } + @Test + void testImageWithJava21() { + Path path = getPath("ubi-java21"); + var result = sut.determine(path); + assertThat(result).hasValueSatisfying(v -> { + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); + assertThat(v.getJavaVersion()).isEqualTo(21); + }); + } + @Test void testUnhandled() { Path path = getPath("openjdk-11-runtime"); diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime index abba4268c7928..eb3b9e643de4c 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.17 +FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime index 4542ffb9d471f..14f5d447dbd36 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime @@ -1,5 +1,5 @@ # Use Java 17 base image -FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17 +FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime new file mode 100644 index 0000000000000..8d11343e7b78e --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime @@ -0,0 +1,18 @@ +# Use Java 17 base image +FROM registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Append additional options to the java process, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 + +ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ] diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 index 9ad8990cf8fb5..64397357e0628 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 index 5ae6e1e2f3ac4..77d59a96bc997 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-17-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 new file mode 100644 index 0000000000000..ee1e916b5d106 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 @@ -0,0 +1,31 @@ +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 + +ARG JAVA_PACKAGE=java-21-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ + && microdnf update \ + && microdnf clean all \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 540 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=1001 target/quarkus-app/*.jar /deployments/ +COPY --chown=1001 target/quarkus-app/app/ /deployments/app/ +COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java index 53a676a68153a..b79d0303741fe 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java @@ -16,9 +16,12 @@ public class ContainerImageJibConfig { /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.17} is used as the default. + * When the application is built against Java 17 or higher (but less than 21), + * {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18} + * is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index d6ac1b57c58c4..c3cb77ddc4a16 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -92,8 +92,10 @@ public class JibProcessor { private static final IsClassPredicate IS_CLASS_PREDICATE = new IsClassPredicate(); private static final String BINARY_NAME_IN_CONTAINER = "application"; - private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17"; - private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.17"; + private static final String JAVA_21_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18"; + private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18"; + private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18"; + private static final String DEFAULT_BASE_IMAGE_USER = "185"; private static final String OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP = "io.opentelemetry.context.contextStorageProvider"; @@ -134,6 +136,9 @@ private String determineBaseJvmImage(ContainerImageJibConfig jibConfig, Compiled } var javaVersion = compiledJavaVersion.getJavaVersion(); + if (javaVersion.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return JAVA_21_BASE_IMAGE; + } if (javaVersion.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { return JAVA_17_BASE_IMAGE; } diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java index 9636ba877651d..f049d4a6692fe 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java @@ -15,8 +15,10 @@ @ConfigRoot(name = "openshift", phase = ConfigPhase.BUILD_TIME) public class ContainerImageOpenshiftConfig { - public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.17"; - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.17"; + public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.18"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.18"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.18"; + public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -29,11 +31,12 @@ public class ContainerImageOpenshiftConfig { public static final String FALLBACK_NATIVE_BINARY_DIRECTORY = "/home/quarkus/"; public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion version) { - switch (version.isJava17OrHigher()) { - case TRUE: - return DEFAULT_BASE_JVM_JDK17_IMAGE; - default: - return DEFAULT_BASE_JVM_JDK11_IMAGE; + if (version.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return DEFAULT_BASE_JVM_JDK21_IMAGE; + } else if (version.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return DEFAULT_BASE_JVM_JDK17_IMAGE; + } else { + return DEFAULT_BASE_JVM_JDK11_IMAGE; } } @@ -48,9 +51,11 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion * The value of this property is used to create an ImageStream for the builder image used in the Openshift build. * When it references images already available in the internal Openshift registry, the corresponding streams are used * instead. - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.17} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.18} + * is used as the default. + * When the application is built against Java [17, 20], {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.17} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java index a636a645f7479..0c88e16cba29e 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java @@ -41,9 +41,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.17} + * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.17} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java index 361aba9082290..10e9f127f17b8 100644 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java +++ b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java @@ -15,15 +15,17 @@ public class ContainerImageS2iConfig { public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11"; public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion version) { - switch (version.isJava17OrHigher()) { - case TRUE: - return DEFAULT_BASE_JVM_JDK17_IMAGE; - default: - return DEFAULT_BASE_JVM_JDK11_IMAGE; + if (version.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return DEFAULT_BASE_JVM_JDK21_IMAGE; + } else if (version.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return DEFAULT_BASE_JVM_JDK17_IMAGE; + } else { + return DEFAULT_BASE_JVM_JDK11_IMAGE; } } diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute index c3dab99e60b8f..eade03aa1a939 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.17 +FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.18 ENV LANGUAGE='en_US:en' diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml index 6cda40b09636a..22e7403ac8e26 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml @@ -5,6 +5,6 @@ language: data: dockerfile: native: - from: registry.access.redhat.com/ubi8/ubi-minimal:8.8 + from: registry.access.redhat.com/ubi8/ubi-minimal:8.9 native-micro: from: quay.io/quarkus/quarkus-micro-image:2.0 diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java index c9585530d468f..e822bd176c4ea 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java @@ -315,13 +315,13 @@ private void checkDockerfilesWithMaven(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./mvnw package")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.17"))//TODO: make a test for java17 + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18"))//TODO: make a test for java17 .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./mvnw package -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.17")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) @@ -341,13 +341,13 @@ private void checkDockerfilesWithGradle(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./gradlew build")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.17"))//TODO: make a test for java17 + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18"))//TODO: make a test for java17 .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./gradlew build -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.17")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) diff --git a/integration-tests/awt/src/main/docker/Dockerfile.native b/integration-tests/awt/src/main/docker/Dockerfile.native index f1fd2c79bd135..e2ff5cf61ed94 100644 --- a/integration-tests/awt/src/main/docker/Dockerfile.native +++ b/integration-tests/awt/src/main/docker/Dockerfile.native @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 # Dependencies for AWT RUN microdnf install freetype fontconfig \ && microdnf clean all diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm index 53ed09abd23c2..566ba7b5a17bf 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm index 53ed09abd23c2..566ba7b5a17bf 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm index 53ed09abd23c2..566ba7b5a17bf 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm index 53ed09abd23c2..566ba7b5a17bf 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm index 53ed09abd23c2..566ba7b5a17bf 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm index 53ed09abd23c2..566ba7b5a17bf 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm index 53ed09abd23c2..566ba7b5a17bf 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm index 53ed09abd23c2..566ba7b5a17bf 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 From 8dfac09db83c3838662022084b32036386bfb752 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Mon, 20 Nov 2023 13:34:12 +0100 Subject: [PATCH 13/24] Fix OpenTelemetry trace exclusion of endpoints served from the management interface (cherry picked from commit 1c55119344d30875fa008e00cea2fd6146f6c2d1) --- .../deployment/tracing/TracerProcessor.java | 17 ++++++++++++++++- .../http/deployment/VertxHttpProcessor.java | 18 +++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java index 63337d717d8f0..b07e5891fbd53 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.opentelemetry.deployment.tracing; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -118,7 +119,21 @@ void dropNames( // Drop framework paths List nonApplicationUris = new ArrayList<>(); frameworkEndpoints.ifPresent( - frameworkEndpointsBuildItem -> nonApplicationUris.addAll(frameworkEndpointsBuildItem.getEndpoints())); + frameworkEndpointsBuildItem -> { + for (String endpoint : frameworkEndpointsBuildItem.getEndpoints()) { + // Management routes are using full urls -> Extract the path. + if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) { + try { + nonApplicationUris.add(new URL(endpoint).getPath()); + } catch (Exception ignored) { // Not an URL + nonApplicationUris.add(endpoint); + } + } else { + nonApplicationUris.add(endpoint); + } + } + }); + dropNonApplicationUris.produce(new DropNonApplicationUrisBuildItem(nonApplicationUris)); // Drop Static Resources diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index fd8ec2b3ce36e..0d6835565655e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -114,16 +114,24 @@ FrameworkEndpointsBuildItem frameworkEndpoints(NonApplicationRootPathBuildItem n for (RouteBuildItem route : routes) { if (FRAMEWORK_ROUTE.equals(route.getRouteType())) { if (route.getConfiguredPathInfo() != null) { - frameworkEndpoints.add(route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath, - managementInterfaceBuildTimeConfig, launchModeBuildItem)); + String endpointPath = route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath, + managementInterfaceBuildTimeConfig, launchModeBuildItem); + frameworkEndpoints.add(endpointPath); continue; } if (route.getRouteFunction() != null && route.getRouteFunction() instanceof BasicRoute) { BasicRoute basicRoute = (BasicRoute) route.getRouteFunction(); if (basicRoute.getPath() != null) { - // Calling TemplateHtmlBuilder does not see very correct here, but it is the underlying API for ConfiguredPathInfo - frameworkEndpoints - .add(adjustRoot(nonApplicationRootPath.getNonApplicationRootPath(), basicRoute.getPath())); + if (basicRoute.getPath().startsWith(nonApplicationRootPath.getNonApplicationRootPath())) { + // Do not repeat the non application root path. + frameworkEndpoints.add(basicRoute.getPath()); + } else { + // Calling TemplateHtmlBuilder does not see very correct here, but it is the underlying API for ConfiguredPathInfo + String adjustRoot = adjustRoot(nonApplicationRootPath.getNonApplicationRootPath(), + basicRoute.getPath()); + frameworkEndpoints.add(adjustRoot); + } + } } } From 04eddd2798ebe61ae340c37c076b61ac963bbd99 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 20 Nov 2023 12:49:41 +0200 Subject: [PATCH 14/24] Add basic Range header support Closes: #37205 (cherry picked from commit b84fcde5a7d22b52d0576dc55963b9fe54103a51) --- .../server/test/providers/FileTestCase.java | 15 ++ .../serialisers/ServerFileBodyHandler.java | 140 +++++++++++++++++- .../serialisers/ServerPathBodyHandler.java | 5 +- 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java index c9adc3def2c48..80f99ddf1105d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java @@ -41,6 +41,21 @@ public void testFiles() throws Exception { .statusCode(200) .header(HttpHeaders.CONTENT_LENGTH, contentLength) .body(Matchers.equalTo(content)); + RestAssured.given().header("Range", "bytes=0-9").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .body(Matchers.equalTo(content.substring(0, 10))); + RestAssured.given().header("Range", "bytes=10-19").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .body(Matchers.equalTo(content.substring(10, 20))); + RestAssured.given().header("Range", "bytes=10-").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(content.length() - 10)) + .body(Matchers.equalTo(content.substring(10))); RestAssured.get("/providers/file/file-partial") .then() .statusCode(200) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java index aa974beede41f..bd33ed659fda6 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java @@ -3,13 +3,18 @@ import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -30,6 +35,139 @@ public boolean isWriteable(Class type, Type genericType, ResteasyReactiveReso @Override public void writeResponse(File o, Type genericType, ServerRequestContext context) throws WebApplicationException { - context.serverResponse().sendFile(o.getAbsolutePath(), 0, o.length()); + sendFile(o, context); + } + + static void sendFile(File file, ServerRequestContext context) { + ResteasyReactiveRequestContext ctx = ((ResteasyReactiveRequestContext) context); + Object rangeObj = ctx.getHeader("Range", true); + ByteRange byteRange = rangeObj == null ? null : ByteRange.parse(rangeObj.toString()); + if ((byteRange != null) && (byteRange.ranges.size() == 1)) { + ByteRange.Range range = byteRange.ranges.get(0); + long length = range.getEnd() == -1 ? Long.MAX_VALUE : range.getEnd() - range.getStart() + 1; + context.serverResponse() + .setStatusCode(Response.Status.PARTIAL_CONTENT.getStatusCode()) + .sendFile(file.getAbsolutePath(), range.getStart(), length); + + } else { + context.serverResponse().sendFile(file.getAbsolutePath(), 0, file.length()); + } + } + + /** + * Represents a byte range for a range request + * + * @author Stuart Douglas + * + * NOTE: copied from Quarkus HTTP + */ + public static class ByteRange { + + private static final Logger log = Logger.getLogger(ByteRange.class); + + private final List ranges; + + public ByteRange(List ranges) { + this.ranges = ranges; + } + + public int getRanges() { + return ranges.size(); + } + + /** + * Gets the start of the specified range segment, of -1 if this is a suffix range segment + * + * @param range The range segment to get + * @return The range start + */ + public long getStart(int range) { + return ranges.get(range).getStart(); + } + + /** + * Gets the end of the specified range segment, or the number of bytes if this is a suffix range segment + * + * @param range The range segment to get + * @return The range end + */ + public long getEnd(int range) { + return ranges.get(range).getEnd(); + } + + /** + * Attempts to parse a range request. If the range request is invalid it will just return null so that + * it may be ignored. + * + * @param rangeHeader The range spec + * @return A range spec, or null if the range header could not be parsed + */ + public static ByteRange parse(String rangeHeader) { + if (rangeHeader == null || rangeHeader.length() < 7) { + return null; + } + if (!rangeHeader.startsWith("bytes=")) { + return null; + } + List ranges = new ArrayList<>(); + String[] parts = rangeHeader.substring(6).split(","); + for (String part : parts) { + try { + int index = part.indexOf('-'); + if (index == 0) { + //suffix range spec + //represents the last N bytes + //internally we represent this using a -1 as the start position + long val = Long.parseLong(part.substring(1)); + if (val < 0) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + ranges.add(new Range(-1, val)); + } else { + if (index == -1) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + long start = Long.parseLong(part.substring(0, index)); + if (start < 0) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + long end; + if (index + 1 < part.length()) { + end = Long.parseLong(part.substring(index + 1)); + } else { + end = -1; + } + ranges.add(new Range(start, end)); + } + } catch (NumberFormatException e) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + } + if (ranges.isEmpty()) { + return null; + } + return new ByteRange(ranges); + } + + public static class Range { + private final long start, end; + + public Range(long start, long end) { + this.start = start; + this.end = end; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + } } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java index 0585d84426286..252ddd98a8dff 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java @@ -12,7 +12,6 @@ import org.jboss.resteasy.reactive.common.providers.serialisers.PathBodyHandler; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; -import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -36,8 +35,6 @@ public boolean isWriteable(Class type, Type genericType, ResteasyReactiveReso @Override public void writeResponse(java.nio.file.Path o, Type genericType, ServerRequestContext context) throws WebApplicationException { - ServerHttpResponse serverResponse = context.serverResponse(); - // sendFile implies end(), even though javadoc doesn't say, if you add end() it will throw - serverResponse.sendFile(o.toString(), 0, Long.MAX_VALUE); + ServerFileBodyHandler.sendFile(o.toFile(), context); } } From df832bf8373bbc9efbdfaec7e7d5e545975fb664 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Fri, 17 Nov 2023 21:32:44 +0100 Subject: [PATCH 15/24] Removed DependencyFlags.REMOVED (cherry picked from commit 6ebbc22aead853b9125f4970f47acf1925cc9049) --- .../deployment/runnerjar/ExcludedArtifactsTest.java | 8 -------- .../bootstrap/model/ApplicationModelBuilder.java | 13 +++++-------- .../quarkus/maven/dependency/DependencyFlags.java | 7 ------- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ExcludedArtifactsTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ExcludedArtifactsTest.java index 89935d3363858..b0db1dfb0cbc0 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ExcludedArtifactsTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ExcludedArtifactsTest.java @@ -78,13 +78,5 @@ protected void assertAppModel(ApplicationModel model) throws Exception { expected.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "dep-g", "1"), DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP)); assertEquals(expected, getDependenciesWithFlag(model, DependencyFlags.RUNTIME_CP)); - - expected = new HashSet<>(); - expected.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-dep", "1"), - DependencyFlags.REMOVED)); - expected.add(new ArtifactDependency(ArtifactCoords.jar("org.banned", "dep-e", "1"), DependencyFlags.REMOVED)); - expected.add(new ArtifactDependency(ArtifactCoords.jar("org.banned.too", "dep-d", "1"), DependencyFlags.REMOVED)); - expected.add(new ArtifactDependency(ArtifactCoords.jar("org.banned", "dep-f", "1"), DependencyFlags.REMOVED)); - assertEquals(expected, getDependenciesWithFlag(model, DependencyFlags.REMOVED)); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java index b4db9c3811fc5..0dcc5c24da325 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java @@ -240,31 +240,28 @@ private boolean isExcluded(ArtifactCoords coords) { List buildDependencies() { for (ArtifactKey key : parentFirstArtifacts) { final ResolvedDependencyBuilder d = dependencies.get(key); - if (d != null && !d.isFlagSet(DependencyFlags.REMOVED)) { + if (d != null) { d.setFlags(DependencyFlags.CLASSLOADER_PARENT_FIRST); } } for (ArtifactKey key : runnerParentFirstArtifacts) { final ResolvedDependencyBuilder d = dependencies.get(key); - if (d != null && !d.isFlagSet(DependencyFlags.REMOVED)) { + if (d != null) { d.setFlags(DependencyFlags.CLASSLOADER_RUNNER_PARENT_FIRST); } } for (ArtifactKey key : lesserPriorityArtifacts) { final ResolvedDependencyBuilder d = dependencies.get(key); - if (d != null && !d.isFlagSet(DependencyFlags.REMOVED)) { + if (d != null) { d.setFlags(DependencyFlags.CLASSLOADER_LESSER_PRIORITY); } } final List result = new ArrayList<>(dependencies.size()); for (ResolvedDependencyBuilder db : this.dependencies.values()) { - if (isExcluded(db.getArtifactCoords())) { - db.setFlags(DependencyFlags.REMOVED); - db.clearFlag(DependencyFlags.DEPLOYMENT_CP); - db.clearFlag(DependencyFlags.RUNTIME_CP); + if (!isExcluded(db.getArtifactCoords())) { + result.add(db.build()); } - result.add(db.build()); } return result; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java index dc700789874bf..8d9c50148784a 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java @@ -23,13 +23,6 @@ public interface DependencyFlags { // once the processing of the whole tree has completed. int VISITED = 0b00100000000000; - /** - * Dependencies that were removed from the application model - * following {@code removed-artifacts} - * configuration properties collected from extension metadata. - */ - int REMOVED = 0b01000000000000; - /* @formatter:on */ } From 30a7164a070d6fa5e46a3dd507602eeae7f7f28d Mon Sep 17 00:00:00 2001 From: Alex Martel <13215031+manofthepeace@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:37:55 -0400 Subject: [PATCH 16/24] Add note that endpointdisabled does not work native (cherry picked from commit 047c5b766f7f7fd619b44f3ddce7c81f4a2a663d) --- docs/src/main/asciidoc/resteasy-reactive.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index a5e4e544f6649..5a4afac21f3d0 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -3006,6 +3006,10 @@ public class RuntimeResource { } } ---- +[IMPORTANT] +==== +This feature does not work when using native build. +==== == RESTEasy Reactive client From 00720c44f8fcbb8d1f8d0989bbf7419d11ab2032 Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Tue, 21 Nov 2023 09:48:58 +0100 Subject: [PATCH 17/24] Allow to retrieve minimum and recommended Java versions from catalog metadata (cherry picked from commit 808fba793396abe0a808c88e60a326a184ec6514) --- .../platform/catalog/processor/CatalogProcessor.java | 8 ++++++++ .../platform/catalog/processor/MetadataValue.java | 2 +- .../src/main/resources/fake-catalog.json | 2 ++ .../io/quarkus/registry/catalog/ExtensionCatalog.java | 3 +++ .../quarkus/platform/catalog/CatalogProcessorTest.java | 9 +++++++++ .../devtools/src/test/resources/platform-metadata.json | 4 +++- 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/CatalogProcessor.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/CatalogProcessor.java index 8fbd392efdce0..ae0037e90f61b 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/CatalogProcessor.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/CatalogProcessor.java @@ -85,4 +85,12 @@ public List getProcessedCategoriesInOrder() { public static MetadataValue getMetadataValue(ExtensionCatalog catalog, String path) { return MetadataValue.get(catalog.getMetadata(), path); } + + public static String getMinimumJavaVersion(ExtensionCatalog catalog) { + return getMetadataValue(catalog, ExtensionCatalog.MD_MINIMUM_JAVA_VERSION).asString(); + } + + public static String getRecommendedJavaVersion(ExtensionCatalog catalog) { + return getMetadataValue(catalog, ExtensionCatalog.MD_RECOMMENDED_JAVA_VERSION).asString(); + } } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/MetadataValue.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/MetadataValue.java index 11ffee9d9a9df..13a71dabb4321 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/MetadataValue.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/MetadataValue.java @@ -5,7 +5,7 @@ import java.util.Locale; import java.util.Map; -final class MetadataValue { +public final class MetadataValue { private static final MetadataValue EMPTY_METADATA_VALUE = new MetadataValue(null); private final Object val; diff --git a/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json b/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json index c8e7e56118368..3b970eaec4e5a 100644 --- a/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json +++ b/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json @@ -386,6 +386,8 @@ "gradle-plugin-id": "io.quarkus", "gradle-plugin-version": "999-FAKE", "supported-maven-versions": "[3.6.2,)", + "minimum-java-version": "11", + "recommended-java-version": "17", "proposed-maven-version": "3.9.5", "maven-wrapper-version": "3.2.0", "gradle-wrapper-version": "8.4" diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/ExtensionCatalog.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/ExtensionCatalog.java index 2bcdb7eef419a..91f7d027e61cb 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/ExtensionCatalog.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/ExtensionCatalog.java @@ -11,6 +11,9 @@ public interface ExtensionCatalog extends ExtensionOrigin { + String MD_MINIMUM_JAVA_VERSION = "project.properties.minimum-java-version"; + String MD_RECOMMENDED_JAVA_VERSION = "project.properties.recommended-java-version"; + /** * All the origins this catalog is derived from. * diff --git a/integration-tests/devtools/src/test/java/io/quarkus/platform/catalog/CatalogProcessorTest.java b/integration-tests/devtools/src/test/java/io/quarkus/platform/catalog/CatalogProcessorTest.java index 0b7f884660c55..83dbe859c7c00 100644 --- a/integration-tests/devtools/src/test/java/io/quarkus/platform/catalog/CatalogProcessorTest.java +++ b/integration-tests/devtools/src/test/java/io/quarkus/platform/catalog/CatalogProcessorTest.java @@ -1,7 +1,9 @@ package io.quarkus.platform.catalog; import static io.quarkus.devtools.testing.FakeExtensionCatalog.newFakeExtensionCatalog; +import static io.quarkus.platform.catalog.processor.CatalogProcessor.getMinimumJavaVersion; import static io.quarkus.platform.catalog.processor.CatalogProcessor.getProcessedCategoriesInOrder; +import static io.quarkus.platform.catalog.processor.CatalogProcessor.getRecommendedJavaVersion; import static org.assertj.core.api.Assertions.assertThat; import java.util.Objects; @@ -27,6 +29,13 @@ void testCategoryOrder() { .startsWith("web", "core", "reactive", "serialization", "compatibility", "alt-languages", "uncategorized"); } + @Test + void testJavaVersions() { + final ExtensionCatalog catalog = newFakeExtensionCatalog(); + assertThat(getMinimumJavaVersion(catalog)).isEqualTo("11"); + assertThat(getRecommendedJavaVersion(catalog)).isEqualTo("17"); + } + @Test void testExtensionsOrder() { final ExtensionCatalog catalog = newFakeExtensionCatalog(); diff --git a/integration-tests/devtools/src/test/resources/platform-metadata.json b/integration-tests/devtools/src/test/resources/platform-metadata.json index 59160da30fb5b..b4b073f5b6adf 100644 --- a/integration-tests/devtools/src/test/resources/platform-metadata.json +++ b/integration-tests/devtools/src/test/resources/platform-metadata.json @@ -43,7 +43,9 @@ "supported-maven-versions": "${supported-maven-versions}", "proposed-maven-version": "${proposed-maven-version}", "maven-wrapper-version": "${maven-wrapper.version}", - "gradle-wrapper-version": "${gradle-wrapper.version}" + "gradle-wrapper-version": "${gradle-wrapper.version}", + "minimum-java-version": "${minimum-java-version}", + "recommended-java-version": "${recommended-java-version}" } }, "codestarts-artifacts": [ From 008344712857ab7ab805fe917843b9f5bfa794f8 Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Tue, 21 Nov 2023 10:03:50 +0100 Subject: [PATCH 18/24] Add method to retrieve the list of compatible java versions (cherry picked from commit 542e2b92cba4e8af11e799736fa9da0ace015f92) --- .../java/io/quarkus/devtools/project/JavaVersion.java | 10 ++++++++++ .../io/quarkus/devtools/project/JavaVersionTest.java | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java index 6dd09ec9c887b..ffa49e92acafb 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java @@ -6,6 +6,7 @@ import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; public final class JavaVersion { @@ -71,6 +72,15 @@ public static int determineBestJavaLtsVersion() { return determineBestJavaLtsVersion(Runtime.version().feature()); } + public static SortedSet getCompatibleLTSVersions(JavaVersion minimumJavaVersion) { + if (minimumJavaVersion.isEmpty()) { + return JAVA_VERSIONS_LTS; + } + return JAVA_VERSIONS_LTS.stream() + .filter(v -> v >= minimumJavaVersion.getAsInt()) + .collect(Collectors.toCollection(TreeSet::new)); + } + public static int determineBestJavaLtsVersion(int runtimeVersion) { int bestLtsVersion = DEFAULT_JAVA_VERSION; for (int ltsVersion : JAVA_VERSIONS_LTS) { diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java index 285a582e81b2c..3e0d82dd75241 100644 --- a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java @@ -1,10 +1,13 @@ package io.quarkus.devtools.project; import static io.quarkus.devtools.project.JavaVersion.DETECT_JAVA_RUNTIME_VERSION; +import static io.quarkus.devtools.project.JavaVersion.JAVA_VERSIONS_LTS; import static io.quarkus.devtools.project.JavaVersion.computeJavaVersion; import static io.quarkus.devtools.project.JavaVersion.determineBestJavaLtsVersion; +import static io.quarkus.devtools.project.JavaVersion.getCompatibleLTSVersions; import static io.quarkus.devtools.project.SourceType.JAVA; import static io.quarkus.devtools.project.SourceType.KOTLIN; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -31,6 +34,14 @@ public void givenJavaVersion18ShouldReturn17() { assertEquals("17", computeJavaVersion(JAVA, "18")); } + @Test + void shouldProperlyUseMinJavaVersion() { + assertThat(getCompatibleLTSVersions(new JavaVersion("11"))).isEqualTo(JAVA_VERSIONS_LTS); + assertThat(getCompatibleLTSVersions(new JavaVersion("17"))).containsExactly(17, 21); + assertThat(getCompatibleLTSVersions(new JavaVersion("100"))).isEmpty(); + assertThat(getCompatibleLTSVersions(JavaVersion.NA)).isEqualTo(JAVA_VERSIONS_LTS); + } + @Test public void givenAutoDetectShouldReturnAppropriateVersion() { final String bestJavaLtsVersion = String.valueOf(determineBestJavaLtsVersion(Runtime.version().feature())); From fca5cb8c1ffa6d267bcdb479ecef5e5a069669ca Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 20 Nov 2023 16:25:02 +0200 Subject: [PATCH 19/24] Allow REST Client to return the entire SSE event This can be useful when the id or the name of the event contain useful metadata Closes: #37107 (cherry picked from commit 6b66359920880494aff9be13945735ca3ab72b9f) --- .../reactive/jackson/test/MultiSseTest.java | 94 +++++++++++++++++++ .../resteasy/reactive/client/SseEvent.java | 15 +++ .../reactive/client/impl/MultiInvoker.java | 44 ++++++++- 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java index aa715e04fb948..780bb6b931694 100644 --- a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java @@ -8,15 +8,21 @@ import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.sse.OutboundSseEvent; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseEventSink; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.jboss.resteasy.reactive.RestStreamElementType; +import org.jboss.resteasy.reactive.client.SseEvent; import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -112,6 +118,63 @@ void shouldRestStreamElementTypeOverwriteProducesAtClassLevel() { .containsExactly(new Dto("foo", "bar"), new Dto("chocolate", "bar"))); } + @Test + void shouldBeAbleReadEntireEvent() { + var resultList = new CopyOnWriteArrayList<>(); + createClient() + .event() + .subscribe().with(new Consumer<>() { + @Override + public void accept(SseEvent event) { + resultList.add(new EventContainer(event.id(), event.name(), event.data())); + } + }); + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> assertThat(resultList).containsExactly( + new EventContainer("id0", "name0", new Dto("name0", "0")), + new EventContainer("id1", "name1", new Dto("name1", "1")))); + } + + static class EventContainer { + final String id; + final String name; + final Dto dto; + + EventContainer(String id, String name, Dto dto) { + this.id = id; + this.name = name; + this.dto = dto; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventContainer that = (EventContainer) o; + return Objects.equals(id, that.id) && Objects.equals(name, that.name) + && Objects.equals(dto, that.dto); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, dto); + } + + @Override + public String toString() { + return "EventContainer{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + ", dto=" + dto + + '}'; + } + } + private SseClient createClient() { return QuarkusRestClientBuilder.newBuilder() .baseUri(uri) @@ -144,6 +207,11 @@ public interface SseClient { @Produces(MediaType.SERVER_SENT_EVENTS) @Path("/with-entity-json") Multi> postAndReadAsMap(String entity); + + @GET + @Path("/event") + @Produces(MediaType.SERVER_SENT_EVENTS) + Multi> event(); } @Path("/sse") @@ -175,6 +243,24 @@ public Multi post(String entity) { public Multi postAndReadAsMap(String entity) { return Multi.createBy().repeating().supplier(() -> new Dto("foo", entity)).atMost(3); } + + @GET + @Path("/event") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void event(@Context SseEventSink sink, @Context Sse sse) { + // send a stream of few events + try (sink) { + for (int i = 0; i < 2; i++) { + final OutboundSseEvent.Builder builder = sse.newEventBuilder(); + builder.id("id" + i) + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .data(Dto.class, new Dto("name" + i, String.valueOf(i))) + .name("name" + i); + + sink.send(builder.build()); + } + } + } } @Path("/sse-rest-stream-element-type") @@ -226,5 +312,13 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(name, value); } + + @Override + public String toString() { + return "Dto{" + + "name='" + name + '\'' + + ", value='" + value + '\'' + + '}'; + } } } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java new file mode 100644 index 0000000000000..a6978b93d2dc7 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java @@ -0,0 +1,15 @@ +package org.jboss.resteasy.reactive.client; + +/** + * Represents the entire SSE response from the server + */ +public interface SseEvent { + + String id(); + + String name(); + + String comment(); + + T data(); +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java index fe6a93492c42f..e483baa0ce357 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java @@ -2,6 +2,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.lang.reflect.ParameterizedType; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -10,6 +11,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.client.SseEvent; import org.jboss.resteasy.reactive.common.jaxrs.ResponseImpl; import org.jboss.resteasy.reactive.common.util.RestMediaType; @@ -151,10 +153,17 @@ private boolean isNewlineDelimited(ResponseImpl response) { RestMediaType.APPLICATION_NDJSON_TYPE.isCompatible(response.getMediaType()); } + @SuppressWarnings({ "unchecked", "rawtypes" }) private void registerForSse(MultiRequest multiRequest, GenericType responseType, Response response, HttpClientResponse vertxResponse, String defaultContentType) { + + boolean returnSseEvent = SseEvent.class.equals(responseType.getRawType()); + GenericType responseTypeFirstParam = responseType.getType() instanceof ParameterizedType + ? new GenericType(((ParameterizedType) responseType.getType()).getActualTypeArguments()[0]) + : null; + // honestly, isn't reconnect contradictory with completion? // FIXME: Reconnect settings? // For now we don't want multi to reconnect @@ -165,10 +174,39 @@ private void registerForSse(MultiRequest multiRequest, sseSource.register(event -> { // DO NOT pass the response mime type because it's SSE: let the event pick between the X-SSE-Content-Type header or // the content-type SSE field - R item = event.readData(responseType); - if (item != null) { // we don't emit null because it breaks Multi (by design) - multiRequest.emit(item); + if (returnSseEvent) { + multiRequest.emit((R) new SseEvent() { + @Override + public String id() { + return event.getId(); + } + + @Override + public String name() { + return event.getName(); + } + + @Override + public String comment() { + return event.getComment(); + } + + @Override + public Object data() { + if (responseTypeFirstParam != null) { + return event.readData(responseTypeFirstParam); + } else { + return event.readData(); // TODO: is this correct? + } + } + }); + } else { + R item = event.readData(responseType); + if (item != null) { // we don't emit null because it breaks Multi (by design) + multiRequest.emit(item); + } } + }, multiRequest::fail, multiRequest::complete); // watch for user cancelling sseSource.registerAfterRequest(vertxResponse); From 71228976ce03a2eb5ae499a3ffee860cfde02060 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 21 Nov 2023 10:56:35 +0200 Subject: [PATCH 20/24] Allow SSE events to be filtered out from REST Client (cherry picked from commit c9d1eeae65a34632ee993b19ab48755e16cdf596) --- .../reactive/jackson/test/MultiSseTest.java | 85 +++++++++++++++++++ .../client/reactive/deployment/DotNames.java | 3 + .../RestClientReactiveProcessor.java | 37 ++++++++ .../resteasy/reactive/client/SseEvent.java | 27 ++++++ .../reactive/client/SseEventFilter.java | 22 +++++ .../reactive/client/impl/MultiInvoker.java | 69 +++++++++++++-- 6 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEventFilter.java diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java index 780bb6b931694..629b881a93bec 100644 --- a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java @@ -9,6 +9,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.function.Predicate; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -23,6 +24,7 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.jboss.resteasy.reactive.RestStreamElementType; import org.jboss.resteasy.reactive.client.SseEvent; +import org.jboss.resteasy.reactive.client.SseEventFilter; import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -136,6 +138,25 @@ public void accept(SseEvent event) { new EventContainer("id1", "name1", new Dto("name1", "1")))); } + @Test + void shouldBeAbleReadEntireEventWhileAlsoBeingAbleToFilterEvents() { + var resultList = new CopyOnWriteArrayList<>(); + createClient() + .eventWithFilter() + .subscribe().with(new Consumer<>() { + @Override + public void accept(SseEvent event) { + resultList.add(new EventContainer(event.id(), event.name(), event.data())); + } + }); + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> assertThat(resultList).containsExactly( + new EventContainer("id", "n0", new Dto("name0", "0")), + new EventContainer("id", "n1", new Dto("name1", "1")), + new EventContainer("id", "n2", new Dto("name2", "2")))); + } + static class EventContainer { final String id; final String name; @@ -212,6 +233,26 @@ public interface SseClient { @Path("/event") @Produces(MediaType.SERVER_SENT_EVENTS) Multi> event(); + + @GET + @Path("/event-with-filter") + @Produces(MediaType.SERVER_SENT_EVENTS) + @SseEventFilter(CustomFilter.class) + Multi> eventWithFilter(); + } + + public static class CustomFilter implements Predicate> { + + @Override + public boolean test(SseEvent event) { + if ("heartbeat".equals(event.id())) { + return false; + } + if ("END".equals(event.data())) { + return false; + } + return true; + } } @Path("/sse") @@ -261,6 +302,50 @@ public void event(@Context SseEventSink sink, @Context Sse sse) { } } } + + @GET + @Path("/event-with-filter") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void eventWithFilter(@Context SseEventSink sink, @Context Sse sse) { + try (sink) { + sink.send(sse.newEventBuilder() + .id("id") + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .data(Dto.class, new Dto("name0", "0")) + .name("n0") + .build()); + + sink.send(sse.newEventBuilder() + .id("heartbeat") + .comment("heartbeat") + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .build()); + + sink.send(sse.newEventBuilder() + .id("id") + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .data(Dto.class, new Dto("name1", "1")) + .name("n1") + .build()); + + sink.send(sse.newEventBuilder() + .id("heartbeat") + .comment("heartbeat") + .build()); + + sink.send(sse.newEventBuilder() + .id("id") + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .data(Dto.class, new Dto("name2", "2")) + .name("n2") + .build()); + + sink.send(sse.newEventBuilder() + .id("end") + .data("END") + .build()); + } + } } @Path("/sse-rest-stream-element-type") diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java index add3e44795d65..f635e470595a4 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java @@ -12,6 +12,7 @@ import org.eclipse.microprofile.rest.client.annotation.RegisterProviders; import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; import org.jboss.jandex.DotName; +import org.jboss.resteasy.reactive.client.SseEventFilter; import io.quarkus.rest.client.reactive.ClientExceptionMapper; import io.quarkus.rest.client.reactive.ClientFormParam; @@ -41,6 +42,8 @@ public class DotNames { static final DotName METHOD = DotName.createSimple(Method.class.getName()); + public static final DotName SSE_EVENT_FILTER = DotName.createSimple(SseEventFilter.class); + private DotNames() { } } 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 49ee3402e3daf..22a4b76f9b69e 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 @@ -64,6 +64,7 @@ import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; @@ -371,6 +372,42 @@ void registerCompressionInterceptors(BuildProducer ref } } + @BuildStep + void handleSseEventFilter(BuildProducer reflectiveClasses, + BeanArchiveIndexBuildItem beanArchiveIndexBuildItem) { + var index = beanArchiveIndexBuildItem.getIndex(); + Collection instances = index.getAnnotations(DotNames.SSE_EVENT_FILTER); + if (instances.isEmpty()) { + return; + } + + List filterClassNames = new ArrayList<>(instances.size()); + for (AnnotationInstance instance : instances) { + if (instance.target().kind() != AnnotationTarget.Kind.METHOD) { + continue; + } + if (instance.value() == null) { + continue; // can't happen + } + Type filterType = instance.value().asClass(); + DotName filterClassName = filterType.name(); + ClassInfo filterClassInfo = index.getClassByName(filterClassName.toString()); + if (filterClassInfo == null) { + log.warn("Unable to find class '" + filterType.name() + "' in index"); + } else if (!filterClassInfo.hasNoArgsConstructor()) { + throw new RestClientDefinitionException( + "Classes used in @SseEventFilter must have a no-args constructor. Offending class is '" + + filterClassName + "'"); + } else { + filterClassNames.add(filterClassName.toString()); + } + } + reflectiveClasses.produce(ReflectiveClassBuildItem + .builder(filterClassNames.toArray(new String[0])) + .constructors(true) + .build()); + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) void addRestClientBeans(Capabilities capabilities, diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java index a6978b93d2dc7..bcbee51c809dc 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java @@ -5,11 +5,38 @@ */ public interface SseEvent { + /** + * Get event identifier. + *

+ * Contains value of SSE {@code "id"} field. This field is optional. Method may return {@code null}, if the event + * identifier is not specified. + * + * @return event id. + */ String id(); + /** + * Get event name. + *

+ * Contains value of SSE {@code "event"} field. This field is optional. Method may return {@code null}, if the event + * name is not specified. + * + * @return event name, or {@code null} if not set. + */ String name(); + /** + * Get a comment string that accompanies the event. + *

+ * Contains value of the comment associated with SSE event. This field is optional. Method may return {@code null}, if + * the event comment is not specified. + * + * @return comment associated with the event. + */ String comment(); + /** + * Get event data. + */ T data(); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEventFilter.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEventFilter.java new file mode 100644 index 0000000000000..d9419dca5dfdb --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEventFilter.java @@ -0,0 +1,22 @@ +package org.jboss.resteasy.reactive.client; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Predicate; + +/** + * Used when not all SSE events streamed from the server should be included in the event stream returned by the client. + *

+ * IMPORTANT: implementations MUST contain a no-args constructor + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SseEventFilter { + + /** + * Predicate which decides whether an event should be included in the event stream returned by the client. + */ + Class>> value(); +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java index e483baa0ce357..4459e66000227 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java @@ -2,16 +2,19 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.GenericType; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.jboss.resteasy.reactive.client.SseEvent; +import org.jboss.resteasy.reactive.client.SseEventFilter; import org.jboss.resteasy.reactive.common.jaxrs.ResponseImpl; import org.jboss.resteasy.reactive.common.util.RestMediaType; @@ -45,8 +48,8 @@ public Multi get(GenericType responseType) { /** * We need this class to work around a bug in Mutiny where we can register our cancel listener - * after the subscription is cancelled and we never get notified - * See https://github.com/smallrye/smallrye-mutiny/issues/417 + * after the subscription is cancelled, and we never get notified + * See ... */ static class MultiRequest { @@ -127,9 +130,11 @@ public Multi method(String name, Entity entity, GenericType respons if (!emitter.isCancelled()) { if (response.getStatus() == 200 && MediaType.SERVER_SENT_EVENTS_TYPE.isCompatible(response.getMediaType())) { - registerForSse(multiRequest, responseType, response, vertxResponse, + registerForSse( + multiRequest, responseType, vertxResponse, (String) restClientRequestContext.getProperties() - .get(RestClientRequestContext.DEFAULT_CONTENT_TYPE_PROP)); + .get(RestClientRequestContext.DEFAULT_CONTENT_TYPE_PROP), + restClientRequestContext.getInvokedMethod()); } else if (response.getStatus() == 200 && RestMediaType.APPLICATION_STREAM_JSON_TYPE.isCompatible(response.getMediaType())) { registerForJsonStream(multiRequest, restClientRequestContext, responseType, response, @@ -156,14 +161,16 @@ private boolean isNewlineDelimited(ResponseImpl response) { @SuppressWarnings({ "unchecked", "rawtypes" }) private void registerForSse(MultiRequest multiRequest, GenericType responseType, - Response response, - HttpClientResponse vertxResponse, String defaultContentType) { + HttpClientResponse vertxResponse, String defaultContentType, + Method invokedMethod) { boolean returnSseEvent = SseEvent.class.equals(responseType.getRawType()); GenericType responseTypeFirstParam = responseType.getType() instanceof ParameterizedType ? new GenericType(((ParameterizedType) responseType.getType()).getActualTypeArguments()[0]) : null; + Predicate> eventPredicate = createEventPredicate(invokedMethod); + // honestly, isn't reconnect contradictory with completion? // FIXME: Reconnect settings? // For now we don't want multi to reconnect @@ -172,8 +179,39 @@ private void registerForSse(MultiRequest multiRequest, multiRequest.onCancel(sseSource::close); sseSource.register(event -> { + + // TODO: we might want to cut down on the allocations here... + + if (eventPredicate != null) { + boolean keep = eventPredicate.test(new SseEvent<>() { + @Override + public String id() { + return event.getId(); + } + + @Override + public String name() { + return event.getName(); + } + + @Override + public String comment() { + return event.getComment(); + } + + @Override + public String data() { + return event.readData(); + } + }); + if (!keep) { + return; + } + } + // DO NOT pass the response mime type because it's SSE: let the event pick between the X-SSE-Content-Type header or // the content-type SSE field + if (returnSseEvent) { multiRequest.emit((R) new SseEvent() { @Override @@ -212,6 +250,23 @@ public Object data() { sseSource.registerAfterRequest(vertxResponse); } + private Predicate> createEventPredicate(Method invokedMethod) { + if (invokedMethod == null) { + return null; // should never happen + } + + SseEventFilter filterAnnotation = invokedMethod.getAnnotation(SseEventFilter.class); + if (filterAnnotation == null) { + return null; + } + + try { + return filterAnnotation.value().getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + private void registerForChunks(MultiRequest multiRequest, RestClientRequestContext restClientRequestContext, GenericType responseType, From 5b4342d8203979a0da9301ce465811c95205148b Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 21 Nov 2023 12:18:26 +0200 Subject: [PATCH 21/24] Document SSE usage in REST Client (cherry picked from commit 6f41d71bf861bb64c3404c85c1ce7856b4c6f62d) --- .../main/asciidoc/rest-client-reactive.adoc | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index 4d1af8480e4d3..b3c063bccdda2 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -883,6 +883,107 @@ If you use a `CompletionStage`, you would need to call the service's method to r This difference comes from the laziness aspect of Mutiny and its subscription protocol. More details about this can be found in https://smallrye.io/smallrye-mutiny/latest/reference/uni-and-multi/[the Mutiny documentation]. +=== Server-Sent Event (SSE) support + +Consuming SSE events is possible simply by declaring the result type as a `io.smallrye.mutiny.Multi`. + +The simplest example is: + +[source, java] +---- +package org.acme.rest.client; + +import io.smallrye.mutiny.Multi; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/sse") +@RegisterRestClient(configKey = "some-api") +public interface SseClient { + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + Multi get(); +} +---- + +[NOTE] +==== +All the IO involved in streaming the SSE results is done in a non-blocking manner. +==== + +Results are not limited to strings - for example when the server returns JSON payload for each event, Quarkus automatically deserializes it into the generic type used in the `Multi`. + +[TIP] +==== +Users can also access the entire SSE event by using the `org.jboss.resteasy.reactive.client.SseEvent` type. + +A simple example where the event payloads are `Long` values is the following: + +[source, java] +---- +package org.acme.rest.client; + +import io.smallrye.mutiny.Uni; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.client.SseEvent; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +@Path("/sse") +@RegisterRestClient(configKey = "some-api") +public interface SseClient { + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + Multi> get(); +} +---- +==== + +==== Filtering out events + +On occasion, the stream of SSE events may contain some events that should not be returned by the client - an example of this is having the server send heartbeat events in order to keep the underlying TCP connection open. +The REST Client supports filtering out such events by providing the `@org.jboss.resteasy.reactive.client.SseEventFilter`. + +Here is an example of filtering out heartbeat events: + +[source,java] +---- +package org.acme.rest.client; + +import io.smallrye.mutiny.Uni; +import java.util.function.Predicate; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.client.SseEvent; +import org.jboss.resteasy.reactive.client.SseEventFilter; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +@Path("/sse") +@RegisterRestClient(configKey = "some-api") +public interface SseClient { + + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + @SseEventFilter(HeartbeatFilter.class) + Multi> get(); + + + class HeartbeatFilter implements Predicate> { + + @Override + public boolean test(SseEvent event) { + return !"heartbeat".equals(event.id()); + } + } +} +---- + == Custom headers support There are a few ways in which you can specify custom headers for your REST calls: From 279e38559e546058a13f79e6b28ba06d666b989d Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Tue, 21 Nov 2023 11:05:46 +0100 Subject: [PATCH 22/24] Updates Infinispan to 14.0.21.Final (cherry picked from commit d2bb831548d019cf342886170ff2d437806612b0) --- 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 8faa6ff92cfd5..055ec2d25c619 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -138,7 +138,7 @@ 2.2 5.10.0 1.5.0 - 14.0.20.Final + 14.0.21.Final 4.6.5.Final 3.1.5 4.1.100.Final From da6ec38723dcd17a3c7b93ff8d72f71ddc64e80c Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Tue, 21 Nov 2023 12:57:34 +0200 Subject: [PATCH 23/24] Support Docker Desktop for building native executables Treat Docker Desktop as "rootless" since the way it binds mounts does not transparently map the host user ID and GID see https://docs.docker.com/desktop/faqs/linuxfaqs/#how-do-i-enable-file-sharing Closes https://github.com/quarkusio/quarkus/issues/37193 (cherry picked from commit 81818c79e4229d1de8579dd5ca0037715417e7d8) --- .../java/io/quarkus/runtime/util/ContainerRuntimeUtil.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index ed538474b8b57..607ead4f24980 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -191,7 +191,10 @@ private static boolean getRootlessStateFor(ContainerRuntime containerRuntime) { final Predicate stringPredicate; // Docker includes just "rootless" under SecurityOptions, while podman includes "rootless: " if (containerRuntime == ContainerRuntime.DOCKER) { - stringPredicate = line -> line.trim().equals("rootless"); + // We also treat Docker Desktop as "rootless" since the way it binds mounts does not + // transparently map the host user ID and GID + // see https://docs.docker.com/desktop/faqs/linuxfaqs/#how-do-i-enable-file-sharing + stringPredicate = line -> line.trim().equals("rootless") || line.contains("desktop-linux"); } else { stringPredicate = line -> line.trim().equals("rootless: true"); } From 6a237394e9b02140b34c1d2db1e003b617b1906d Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 21 Nov 2023 10:38:52 +0100 Subject: [PATCH 24/24] Build cache - Only store if the access key is around (cherry picked from commit e45dec61524df2cd1f6c640a933e2bd35123ffaf) --- .mvn/gradle-enterprise.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/gradle-enterprise.xml b/.mvn/gradle-enterprise.xml index 0f9d7558936b5..27ac5553de6a2 100644 --- a/.mvn/gradle-enterprise.xml +++ b/.mvn/gradle-enterprise.xml @@ -27,7 +27,7 @@ true - #{env['CI'] != null} + #{env['CI'] != null and env['GRADLE_ENTERPRISE_ACCESS_KEY'] != null and env['GRADLE_ENTERPRISE_ACCESS_KEY'] != ''}