From a1b55f53881f6dbb0c995b7c76db083a18ca9502 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 20 Dec 2023 20:25:17 +0000 Subject: [PATCH 01/36] Split OIDC session cookie if its size is more than 4KB --- .../runtime/CodeAuthenticationMechanism.java | 68 ++++++++-------- .../runtime/DefaultTokenStateManager.java | 4 +- .../quarkus/oidc/runtime/OidcSessionImpl.java | 21 ++--- .../io/quarkus/oidc/runtime/OidcUtils.java | 77 ++++++++++++++++++- .../quarkus/oidc/runtime/OidcUtilsTest.java | 55 +++++++++++++ .../it/keycloak/CustomTenantResolver.java | 6 +- .../io/quarkus/it/keycloak/TenantNonce.java | 1 + .../io/quarkus/it/keycloak/CodeFlowTest.java | 66 +++++++++++++--- 8 files changed, 230 insertions(+), 68 deletions(-) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index b451675273551..08a1829ba87a1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -65,12 +65,10 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha static final String AMP = "&"; static final String EQ = "="; static final String COMMA = ","; - static final String UNDERSCORE = "_"; static final String COOKIE_DELIM = "|"; static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM); static final String STATE_COOKIE_RESTORE_PATH = "restore-path"; static final Uni VOID_UNI = Uni.createFrom().voidItem(); - static final Integer MAX_COOKIE_VALUE_LENGTH = 4096; static final String NO_OIDC_COOKIES_AVAILABLE = "no_oidc_cookies"; private static final String INTERNAL_IDTOKEN_HEADER = "internal"; @@ -88,19 +86,17 @@ public CodeAuthenticationMechanism(BlockingSecurityExecutor blockingExecutor) { public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager, OidcTenantConfig oidcTenantConfig) { final Map cookies = context.request().cookieMap(); - - final Cookie sessionCookie = cookies.get(getSessionCookieName(oidcTenantConfig)); + final String sessionCookieValue = OidcUtils.getSessionCookie(context.data(), cookies, oidcTenantConfig); // If the session is already established then try to re-authenticate - if (sessionCookie != null) { + if (sessionCookieValue != null) { LOG.debug("Session cookie is present, starting the reauthentication"); - context.put(OidcUtils.SESSION_COOKIE_NAME, sessionCookie.getName()); Uni resolvedContext = resolver.resolveContext(context); return resolvedContext.onItem() .transformToUni(new Function>() { @Override public Uni apply(TenantConfigContext tenantContext) { - return reAuthenticate(sessionCookie, context, identityProviderManager, tenantContext); + return reAuthenticate(sessionCookieValue, context, identityProviderManager, tenantContext); } }); } @@ -299,14 +295,14 @@ private String getRequestParametersAsQuery(URI requestUri, MultiMap requestParam } } - private Uni reAuthenticate(Cookie sessionCookie, + private Uni reAuthenticate(String sessionCookie, RoutingContext context, IdentityProviderManager identityProviderManager, TenantConfigContext configContext) { context.put(TenantConfigContext.class.getName(), configContext); return resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig, - sessionCookie.getValue(), getTokenStateRequestContext) + sessionCookie, getTokenStateRequestContext) .onFailure(AuthenticationCompletionException.class) .recoverWithUni( new Function>() { @@ -958,14 +954,14 @@ public Uni apply(Void t) { @Override public Void apply(String cookieValue) { - String sessionCookie = createCookie(context, configContext.oidcConfig, - getSessionCookieName(configContext.oidcConfig), - cookieValue, sessionMaxAge, true).getValue(); - if (sessionCookie.length() >= MAX_COOKIE_VALUE_LENGTH) { - LOG.warnf( - "Session cookie length for the tenant %s is equal or greater than %d bytes." - + " Browsers may ignore this cookie which will cause a new challenge for the authenticated users." - + " Recommendations: 1. Set 'quarkus.oidc.token-state-manager.split-tokens=true'" + String sessionName = OidcUtils.getSessionCookieName(configContext.oidcConfig); + LOG.debugf("Session cookie length for the tenant %s is %d bytes.", + configContext.oidcConfig.tenantId.get(), cookieValue.length()); + if (cookieValue.length() > OidcUtils.MAX_COOKIE_VALUE_LENGTH) { + LOG.debugf( + "Session cookie length is greater than %d bytes." + + " The cookie will be split to chunks to avoid browsers ignoring it." + + " Alternative recommendations: 1. Set 'quarkus.oidc.token-state-manager.split-tokens=true'" + " to have the ID, access and refresh tokens stored in separate cookies." + " 2. Set 'quarkus.oidc.token-state-manager.strategy=id-refresh-tokens' if you do not need to use the access token" + " as a source of roles or to request UserInfo or propagate it to the downstream services." @@ -973,7 +969,22 @@ public Void apply(String cookieValue) { + " but only if it is considered to be safe in your application's network." + " 4. Register a custom 'quarkus.oidc.TokenStateManager' CDI bean with the alternative priority set to 1.", configContext.oidcConfig.tenantId.get(), - MAX_COOKIE_VALUE_LENGTH); + OidcUtils.MAX_COOKIE_VALUE_LENGTH); + for (int sessionIndex = 1, + currentPos = 0; currentPos < cookieValue.length(); sessionIndex++) { + int nextPos = currentPos + OidcUtils.MAX_COOKIE_VALUE_LENGTH; + int nextValueUpperPos = nextPos < cookieValue.length() ? nextPos + : cookieValue.length(); + String nextValue = cookieValue.substring(currentPos, nextValueUpperPos); + // q_session_session_chunk_1, etc + String nextName = sessionName + OidcUtils.SESSION_COOKIE_CHUNK + sessionIndex; + createCookie(context, configContext.oidcConfig, nextName, nextValue, + sessionMaxAge, true); + currentPos = nextPos; + } + } else { + createCookie(context, configContext.oidcConfig, sessionName, cookieValue, + sessionMaxAge, true); } fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity); return null; @@ -1307,30 +1318,15 @@ public Void apply(Void t) { } private static String getStateCookieName(OidcTenantConfig oidcConfig) { - return OidcUtils.STATE_COOKIE_NAME + getCookieSuffix(oidcConfig); + return OidcUtils.STATE_COOKIE_NAME + OidcUtils.getCookieSuffix(oidcConfig); } private static String getPostLogoutCookieName(OidcTenantConfig oidcConfig) { - return OidcUtils.POST_LOGOUT_COOKIE_NAME + getCookieSuffix(oidcConfig); - } - - private static String getSessionCookieName(OidcTenantConfig oidcConfig) { - return OidcUtils.SESSION_COOKIE_NAME + getCookieSuffix(oidcConfig); + return OidcUtils.POST_LOGOUT_COOKIE_NAME + OidcUtils.getCookieSuffix(oidcConfig); } private Uni removeSessionCookie(RoutingContext context, OidcTenantConfig oidcConfig) { - String cookieName = getSessionCookieName(oidcConfig); - return OidcUtils.removeSessionCookie(context, oidcConfig, cookieName, resolver.getTokenStateManager()); - } - - static String getCookieSuffix(OidcTenantConfig oidcConfig) { - String tenantId = oidcConfig.tenantId.get(); - boolean cookieSuffixConfigured = oidcConfig.authentication.cookieSuffix.isPresent(); - String tenantIdSuffix = (cookieSuffixConfigured || !"Default".equals(tenantId)) ? UNDERSCORE + tenantId : ""; - - return cookieSuffixConfigured - ? (tenantIdSuffix + UNDERSCORE + oidcConfig.authentication.cookieSuffix.get()) - : tenantIdSuffix; + return OidcUtils.removeSessionCookie(context, oidcConfig, resolver.getTokenStateManager()); } private class LogoutCall implements Function> { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java index fcc50bfe52a5f..bf23c7bf5f496 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java @@ -136,12 +136,12 @@ private static ServerCookie getRefreshTokenCookie(RoutingContext routingContext, } private static String getAccessTokenCookieName(OidcTenantConfig oidcConfig) { - String cookieSuffix = CodeAuthenticationMechanism.getCookieSuffix(oidcConfig); + String cookieSuffix = OidcUtils.getCookieSuffix(oidcConfig); return SESSION_AT_COOKIE_NAME + cookieSuffix; } private static String getRefreshTokenCookieName(OidcTenantConfig oidcConfig) { - String cookieSuffix = CodeAuthenticationMechanism.getCookieSuffix(oidcConfig); + String cookieSuffix = OidcUtils.getCookieSuffix(oidcConfig); return SESSION_RT_COOKIE_NAME + cookieSuffix; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcSessionImpl.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcSessionImpl.java index 1d9ca56847639..5972902066f09 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcSessionImpl.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcSessionImpl.java @@ -35,20 +35,15 @@ public String getTenantId() { @Override public Uni logout() { - String sessionCookieName = routingContext.get(OidcUtils.SESSION_COOKIE_NAME); - if (sessionCookieName != null) { - Uni oidcConfigUni = resolver.resolveConfig(routingContext); - return oidcConfigUni.onItem().transformToUni(new Function>() { + Uni oidcConfigUni = resolver.resolveConfig(routingContext); + return oidcConfigUni.onItem().transformToUni(new Function>() { + @Override + public Uni apply(OidcTenantConfig oidcConfig) { + return OidcUtils.removeSessionCookie(routingContext, oidcConfig, + resolver.getTokenStateManager()); + } + }); - @Override - public Uni apply(OidcTenantConfig oidcConfig) { - return OidcUtils.removeSessionCookie(routingContext, oidcConfig, sessionCookieName, - resolver.getTokenStateManager()); - } - - }); - } - return Uni.createFrom().voidItem(); } @Override diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 8fcc88f5c15ce..94220d432211f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -13,10 +13,13 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.SortedMap; import java.util.StringTokenizer; +import java.util.TreeMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Pattern; @@ -56,6 +59,7 @@ import io.smallrye.mutiny.subscription.UniEmitter; import io.vertx.core.Handler; import io.vertx.core.MultiMap; +import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.impl.ServerCookie; @@ -72,8 +76,11 @@ public final class OidcUtils { public static final String TENANT_ID_ATTRIBUTE = "tenant-id"; public static final String DEFAULT_TENANT_ID = "Default"; public static final String SESSION_COOKIE_NAME = "q_session"; + public static final String SESSION_COOKIE_CHUNK = "_chunk_"; public static final String STATE_COOKIE_NAME = "q_auth"; + public static final Integer MAX_COOKIE_VALUE_LENGTH = 4096; public static final String POST_LOGOUT_COOKIE_NAME = "q_post_logout"; + static final String UNDERSCORE = "_"; static final Uni VOID_UNI = Uni.createFrom().voidItem(); static final BlockingTaskRunner deleteTokensRequestContext = new BlockingTaskRunner(); @@ -88,6 +95,64 @@ private OidcUtils() { } + public static String getSessionCookie(Map context, Map cookies, + OidcTenantConfig oidcTenantConfig) { + if (cookies.isEmpty()) { + return null; + } + final String sessionCookieName = OidcUtils.getSessionCookieName(oidcTenantConfig); + + if (cookies.containsKey(sessionCookieName)) { + context.put(OidcUtils.SESSION_COOKIE_NAME, List.of(sessionCookieName)); + return cookies.get(sessionCookieName).getValue(); + } else { + final String sessionChunkPrefix = sessionCookieName + OidcUtils.SESSION_COOKIE_CHUNK; + + SortedMap sessionCookies = new TreeMap<>(new Comparator() { + + @Override + public int compare(String s1, String s2) { + // at this point it is guaranteed cookie names end with `chunk_` + int lastUnderscoreIndex1 = s1.lastIndexOf(UNDERSCORE); + int lastUnderscoreIndex2 = s2.lastIndexOf(UNDERSCORE); + Integer pos1 = Integer.valueOf(s1.substring(lastUnderscoreIndex1 + 1)); + Integer pos2 = Integer.valueOf(s2.substring(lastUnderscoreIndex2 + 1)); + return pos1.compareTo(pos2); + } + + }); + for (String cookieName : cookies.keySet()) { + if (cookieName.startsWith(sessionChunkPrefix)) { + sessionCookies.put(cookieName, cookies.get(cookieName).getValue()); + } + } + if (!sessionCookies.isEmpty()) { + context.put(OidcUtils.SESSION_COOKIE_NAME, new ArrayList(sessionCookies.keySet())); + + StringBuilder sessionCookieValue = new StringBuilder(); + for (String value : sessionCookies.values()) { + sessionCookieValue.append(value); + } + return sessionCookieValue.toString(); + } + } + return null; + } + + public static String getSessionCookieName(OidcTenantConfig oidcConfig) { + return OidcUtils.SESSION_COOKIE_NAME + getCookieSuffix(oidcConfig); + } + + public static String getCookieSuffix(OidcTenantConfig oidcConfig) { + String tenantId = oidcConfig.tenantId.get(); + boolean cookieSuffixConfigured = oidcConfig.authentication.cookieSuffix.isPresent(); + String tenantIdSuffix = (cookieSuffixConfigured || !"Default".equals(tenantId)) ? UNDERSCORE + tenantId : ""; + + return cookieSuffixConfigured + ? (tenantIdSuffix + UNDERSCORE + oidcConfig.authentication.cookieSuffix.get()) + : tenantIdSuffix; + } + public static boolean isServiceApp(OidcTenantConfig oidcConfig) { return ApplicationType.SERVICE.equals(oidcConfig.applicationType.orElse(ApplicationType.SERVICE)); } @@ -380,11 +445,15 @@ public static void validatePrimaryJwtTokenType(OidcTenantConfig.Token tokenConfi } } - static Uni removeSessionCookie(RoutingContext context, OidcTenantConfig oidcConfig, String cookieName, + static Uni removeSessionCookie(RoutingContext context, OidcTenantConfig oidcConfig, TokenStateManager tokenStateManager) { - String cookieValue = removeCookie(context, oidcConfig, cookieName); - if (cookieValue != null) { - return tokenStateManager.deleteTokens(context, oidcConfig, cookieValue, + List cookieNames = context.get(SESSION_COOKIE_NAME); + if (cookieNames != null) { + StringBuilder cookieValue = new StringBuilder(); + for (String cookieName : cookieNames) { + cookieValue.append(removeCookie(context, oidcConfig, cookieName)); + } + return tokenStateManager.deleteTokens(context, oidcConfig, cookieValue.toString(), deleteTokensRequestContext); } else { return VOID_UNI; diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java index b2ac3a6788151..3770db039424a 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java @@ -12,8 +12,11 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.security.Permission; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.crypto.SecretKey; @@ -30,10 +33,62 @@ import io.quarkus.oidc.runtime.providers.KnownOidcProviders; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; +import io.vertx.core.http.Cookie; +import io.vertx.core.http.impl.CookieImpl; import io.vertx.core.json.JsonObject; public class OidcUtilsTest { + @Test + public void testGetSingleSessionCookie() throws Exception { + + OidcTenantConfig oidcConfig = new OidcTenantConfig(); + oidcConfig.setTenantId("test"); + Map context = new HashMap<>(); + String sessionCookieValue = OidcUtils.getSessionCookie(context, + Map.of("q_session_test", new CookieImpl("q_session_test", "tokens")), oidcConfig); + assertEquals("tokens", sessionCookieValue); + @SuppressWarnings({ "rawtypes", "unchecked" }) + List names = (List) context.get(OidcUtils.SESSION_COOKIE_NAME); + assertEquals(1, names.size()); + assertEquals("q_session_test", names.get(0)); + } + + @Test + public void testGetMultipleSessionCookies() throws Exception { + + OidcTenantConfig oidcConfig = new OidcTenantConfig(); + oidcConfig.setTenantId("test"); + + char[] alphabet = "abcdefghijklmnopqrstuvwxyz".toCharArray(); + + StringBuilder expectedCookieValue = new StringBuilder(); + Map cookies = new HashMap<>(); + for (int i = 0; i < alphabet.length; i++) { + char[] data = new char[OidcUtils.MAX_COOKIE_VALUE_LENGTH]; + Arrays.fill(data, alphabet[i]); + String cookieName = "q_session_test_chunk_" + (i + 1); + String nextChunk = new String(data); + expectedCookieValue.append(nextChunk); + cookies.put(cookieName, new CookieImpl(cookieName, nextChunk)); + } + String lastChunk = String.valueOf("tokens"); + expectedCookieValue.append(lastChunk); + String lastCookieName = "q_session_test_chunk_" + (alphabet.length + 1); + cookies.put(lastCookieName, new CookieImpl(lastCookieName, lastChunk)); + + Map context = new HashMap<>(); + String sessionCookieValue = OidcUtils.getSessionCookie(context, cookies, oidcConfig); + assertEquals(expectedCookieValue.toString(), sessionCookieValue); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + List names = (List) context.get(OidcUtils.SESSION_COOKIE_NAME); + assertEquals(alphabet.length + 1, names.size()); + for (int i = 0; i < names.size(); i++) { + assertEquals("q_session_test_chunk_" + (i + 1), names.get(i)); + } + } + @Test public void testAcceptGitHubProperties() throws Exception { OidcTenantConfig tenant = new OidcTenantConfig(); diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 8763bf2ef5ebf..2915157d827e8 100644 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -57,7 +57,8 @@ public String resolve(RoutingContext context) { } if (path.contains("tenant-https")) { - if (context.getCookie("q_session_tenant-https_test") != null) { + if (context.getCookie("q_session_tenant-https_test_chunk_1") != null + && context.getCookie("q_session_tenant-https_test_chunk_2") != null) { context.put("reauthenticated", "true"); return context.get(OidcUtils.TENANT_ID_ATTRIBUTE); } else { @@ -66,7 +67,8 @@ public String resolve(RoutingContext context) { } if (path.contains("tenant-nonce")) { - if (context.getCookie("q_session_tenant-nonce") != null) { + if (context.getCookie("q_session_tenant-nonce_chunk_1") != null + && context.getCookie("q_session_tenant-nonce_chunk_2") != null) { context.put("reauthenticated", "true"); return context.get(OidcUtils.TENANT_ID_ATTRIBUTE); } else { diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TenantNonce.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TenantNonce.java index ec3a17cbf2df2..fa6843a367221 100644 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TenantNonce.java +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TenantNonce.java @@ -19,6 +19,7 @@ public class TenantNonce { @GET @Authenticated public String getTenant() { + session.logout().await().indefinitely(); return session.getTenantId() + (routingContext.get("reauthenticated") != null ? ":reauthenticated" : ""); } } diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index 8d55c8193dc06..f1b544141b972 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -13,7 +13,11 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -24,6 +28,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import com.gargoylesoftware.htmlunit.CookieManager; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; import com.gargoylesoftware.htmlunit.WebClient; @@ -221,9 +226,11 @@ public void testCodeFlowForceHttpsRedirectUriAndPkce() throws Exception { page = webClient.getPage(endpointLocationWithoutQueryUri.toURL()); assertEquals("tenant-https:reauthenticated", page.getBody().asNormalizedText()); - Cookie sessionCookie = getSessionCookie(webClient, "tenant-https_test"); - assertNotNull(sessionCookie); - assertEquals("strict", sessionCookie.getSameSite()); + + List sessionCookies = verifyTenantHttpTestCookies(webClient); + + assertEquals("strict", sessionCookies.get(0).getSameSite()); + assertEquals("strict", sessionCookies.get(1).getSameSite()); webClient.getCookieManager().clearCookies(); } } @@ -333,15 +340,19 @@ public void testCodeFlowForceHttpsRedirectUriWithQueryAndPkce() throws Exception URI endpointLocationWithoutQueryUri = URI.create(endpointLocationWithoutQuery); assertEquals("code=b", endpointLocationWithoutQueryUri.getRawQuery()); - Cookie sessionCookie = getSessionCookie(webClient, "tenant-https_test"); - assertNotNull(sessionCookie); + List sessionCookies = verifyTenantHttpTestCookies(webClient); + + StringBuilder sessionCookieValue = new StringBuilder(); + for (Cookie c : sessionCookies) { + sessionCookieValue.append(c.getValue()); + } SecretKey key = new SecretKeySpec(OidcUtils .getSha256Digest("secret".getBytes(StandardCharsets.UTF_8)), "AES"); - String sessionCookieValue = OidcUtils.decryptString(sessionCookie.getValue(), key); + String decryptedSessionCookieValue = OidcUtils.decryptString(sessionCookieValue.toString(), key); - String encodedIdToken = sessionCookieValue.split("\\|")[0]; + String encodedIdToken = decryptedSessionCookieValue.split("\\|")[0]; JsonObject idToken = OidcUtils.decodeJwtContent(encodedIdToken); String expiresAt = idToken.getInteger("exp").toString(); @@ -351,12 +362,22 @@ public void testCodeFlowForceHttpsRedirectUriWithQueryAndPkce() throws Exception response.startsWith("tenant-https:reauthenticated?code=b&expiresAt=" + expiresAt + "&expiresInDuration=")); Integer duration = Integer.valueOf(response.substring(response.length() - 1)); assertTrue(duration > 1 && duration < 5); - sessionCookie = getSessionCookie(webClient, "tenant-https_test"); - assertNotNull(sessionCookie); + + verifyTenantHttpTestCookies(webClient); + webClient.getCookieManager().clearCookies(); } } + private List verifyTenantHttpTestCookies(WebClient webClient) { + List sessionCookies = getSessionCookies(webClient, "tenant-https_test"); + assertNotNull(sessionCookies); + assertEquals(2, sessionCookies.size()); + assertEquals("q_session_tenant-https_test_chunk_1", sessionCookies.get(0).getName()); + assertEquals("q_session_tenant-https_test_chunk_2", sessionCookies.get(1).getName()); + return sessionCookies; + } + @Test public void testCodeFlowNonce() throws Exception { try (final WebClient webClient = createWebClient()) { @@ -391,13 +412,23 @@ public void testCodeFlowNonce() throws Exception { assertEquals(302, webResponse.getStatusCode()); assertNull(getStateCookie(webClient, "tenant-nonce")); + // At this point the session cookie is already available, this 2nd redirect only drops + // OIDC code flow parameters such as `code` and `state` + List sessionCookies = getSessionCookies(webClient, "tenant-nonce"); + assertNotNull(sessionCookies); + assertEquals(2, sessionCookies.size()); + assertEquals("q_session_tenant-nonce_chunk_1", sessionCookies.get(0).getName()); + assertEquals("q_session_tenant-nonce_chunk_2", sessionCookies.get(1).getName()); + String endpointLocationWithoutQuery = webResponse.getResponseHeaderValue("location"); URI endpointLocationWithoutQueryUri = URI.create(endpointLocationWithoutQuery); + // This request will reach the `TenantNonce` endpoint which will also clear the session. page = webClient.getPage(endpointLocationWithoutQueryUri.toURL()); assertEquals("tenant-nonce:reauthenticated", page.getBody().asNormalizedText()); - Cookie sessionCookie = getSessionCookie(webClient, "tenant-nonce"); - assertNotNull(sessionCookie); + + // both cookies should be gone now. + assertNull(getSessionCookies(webClient, "tenant-nonce")); webClient.getCookieManager().clearCookies(); } } @@ -1473,6 +1504,19 @@ private Cookie getSessionCookie(WebClient webClient, String tenantId) { return webClient.getCookieManager().getCookie("q_session" + (tenantId == null ? "_Default_test" : "_" + tenantId)); } + private List getSessionCookies(WebClient webClient, String tenantId) { + String sessionCookieNameChunk = "q_session" + (tenantId == null ? "_Default_test" : "_" + tenantId) + "_chunk_"; + CookieManager cookieManager = webClient.getCookieManager(); + SortedMap sessionCookies = new TreeMap<>(); + for (Cookie cookie : cookieManager.getCookies()) { + if (cookie.getName().startsWith(sessionCookieNameChunk)) { + sessionCookies.put(cookie.getName(), cookie); + } + } + + return sessionCookies.isEmpty() ? null : new ArrayList(sessionCookies.values()); + } + private Cookie getSessionAtCookie(WebClient webClient, String tenantId) { return webClient.getCookieManager().getCookie("q_session_at" + (tenantId == null ? "_Default_test" : "_" + tenantId)); } From 1d648ece44517e97f513c0eac93fff30097ce480 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 21 Dec 2023 16:20:42 +0100 Subject: [PATCH 02/36] Upgrade to Kotlin 1.9.22 --- bom/application/pom.xml | 2 +- build-parent/pom.xml | 2 +- .../src/test/java/io/quarkus/gradle/QuarkusPluginTest.java | 2 +- devtools/gradle/gradle/libs.versions.toml | 2 +- independent-projects/arc/pom.xml | 2 +- .../conditional-dependencies-kotlin/build.gradle.kts | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 982c01e69c3ca..0af2cbe36cd92 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -160,7 +160,7 @@ 2.15.0 2.2.0 1.0.0 - 1.9.21 + 1.9.22 1.7.3 0.27.0 1.6.2 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 21a5223eeb88c..c6590104f2e8b 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -20,7 +20,7 @@ 3.11.0 - 1.9.21 + 1.9.22 1.9.10 2.13.12 4.8.1 diff --git a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java index 862ad0a62abb6..74f976e2b4b7a 100644 --- a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java +++ b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java @@ -105,7 +105,7 @@ public void shouldReturnMultipleOutputSourceDirectories() { @Test public void shouldNotFailOnProjectDependenciesWithoutMain(@TempDir Path testProjectDir) throws IOException { - var kotlinVersion = System.getProperty("kotlin_version", "1.9.21"); + var kotlinVersion = System.getProperty("kotlin_version", "1.9.22"); var settingFile = testProjectDir.resolve("settings.gradle.kts"); var mppProjectDir = testProjectDir.resolve("mpp"); var quarkusProjectDir = testProjectDir.resolve("quarkus"); diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index 5bb10bc0b84fc..a8f4fe2d5dfa3 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -2,7 +2,7 @@ plugin-publish = "1.2.1" # updating Kotlin here makes QuarkusPluginTest > shouldNotFailOnProjectDependenciesWithoutMain(Path) fail -kotlin = "1.9.21" +kotlin = "1.9.22" smallrye-config = "3.4.4" junit5 = "5.10.1" diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 916903d1b1eee..111f04d6b1866 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -52,7 +52,7 @@ 3.24.2 5.10.1 - 1.9.21 + 1.9.22 1.7.3 5.8.0 diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts b/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts index c2b7e6d1cc051..df0225c827b4b 100644 --- a/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("jvm") version "1.9.21" - kotlin("plugin.allopen") version "1.9.21" + kotlin("jvm") version "1.9.22" + kotlin("plugin.allopen") version "1.9.22" id("io.quarkus") } From 0363cdffff4a5bdaca57e98ece8024daf65aee51 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 12 Dec 2023 12:51:07 +0000 Subject: [PATCH 03/36] Improve OIDC code flow concept document --- .../security-oidc-code-flow-authentication.adoc | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index 1cb6e84bf9f72..6dd7a688dac15 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -374,10 +374,11 @@ quarkus.oidc.authentication.extra-params.response_mode=query ==== Customizing the authentication error response -If the user authentication fails at the OIDC authorization endpoint, then the provider will redirect the user back to Quarkus with `error` and `error_description` parameters instead of `code`. +When the user is redirected to the OIDC authorization endpoint to authenticate and, if necessary, authorize the Quarkus application, this redirect request may fail, for example, when an invalid scope is included in the redirect URI. In such cases, the provider will redirect the user back to Quarkus with `error` and `error_description` parameters instead of the expected `code` parameter. + For example, this can happen when an invalid scope or other invalid parameters are included in the redirect to the provider. -In such cases, an HTTP `401`error will be returned by default. +In such cases, an HTTP `401` error will be returned by default. However, you can instead request that a custom public error endpoint is called to return a more user-friendly HTML error page. To do this, set the `quarkus.oidc.authentication.error-path` property, as shown in the following example: @@ -452,7 +453,9 @@ public class ProtectedResource { //or //String rawAccessToken = accessTokenCredential.getToken(); - // Use the raw access token to access a remote endpoint + // Use the raw access token to access a remote endpoint. + // For example, use RestClient to set this token as a `Bearer` scheme value of the HTTP `Authorization` header: + // `Authorization: Bearer rawAccessToken`. return getReservationfromRemoteEndpoint(rawAccesstoken); } } @@ -486,7 +489,7 @@ The way the roles are mapped to the SecurityIdentity roles from the verified tok [NOTE] ==== -If you use Keycloak, set a `microprofile_jwt` client scope for ID token to contain a `groups` claim. +If you use Keycloak, set a `microprofile-jwt` client scope for ID token to contain a `groups` claim. For more information, see the link:https://www.keycloak.org/docs/latest/server_admin/#protocol[Keycloak Server Administration Guide]. ==== @@ -609,7 +612,7 @@ In such cases, use the `quarkus.oidc.token-state-manager.strategy` property to c |To... |Set the property to ... |Keep the ID and refresh tokens only -|`quarkus.oidc.token-state-manager.strategy=id-refresh-token` +|`quarkus.oidc.token-state-manager.strategy=id-refresh-tokens` |Keep the ID token only |`quarkus.oidc.token-state-manager.strategy=id-token` From b4baa1a0ff53c3b926fbcc690ded138d1d2cfd39 Mon Sep 17 00:00:00 2001 From: Welton Rodrigo Torres Nascimento Date: Mon, 25 Dec 2023 15:15:41 -0300 Subject: [PATCH 04/36] Update getting-started-testing.adoc fix typo --- docs/src/main/asciidoc/getting-started-testing.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 31639cfb3af85..7b3537015ebaa 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1088,7 +1088,7 @@ or starting a mock HTTP server using https://wiremock.org/[Wiremock] (an example === Altering the test class -When creating a custom `QuarkusTestResourceLifecycleManager` that needs to inject the something into the test class, the `inject` methods can be used. +When creating a custom `QuarkusTestResourceLifecycleManager` that needs to inject something into the test class, the `inject` methods can be used. If for example you have a test like the following: [source,java] From 4a9b2126fe3c332dcd4489ff42a65adf2b55d484 Mon Sep 17 00:00:00 2001 From: Welton Rodrigo Torres Nascimento Date: Mon, 25 Dec 2023 15:32:28 -0300 Subject: [PATCH 05/36] Update getting-started-testing.adoc Add information abot how to load configuration values inside a `QuarkusTestResourceLifecycleManager` --- docs/src/main/asciidoc/getting-started-testing.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 31639cfb3af85..b3729cea4cf90 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1086,6 +1086,8 @@ but it is common to create custom implementations to address specific applicatio Common cases include starting docker containers using https://www.testcontainers.org/[Testcontainers] (an example of which can be found https://github.com/quarkusio/quarkus/blob/main/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java[here]), or starting a mock HTTP server using https://wiremock.org/[Wiremock] (an example of which can be found https://github.com/geoand/quarkus-test-demo/blob/main/src/test/java/org/acme/getting/started/country/WiremockCountries.java[here]). +NOTE: As `QuarkusTestResourceLifecycleManager` is not a CDI Bean, classes that implement it can't have fields injected with `@Inject`. You can use `String propertyName = ConfigProvider.getConfig().getValue("quarkus.my-config-group.myconfig", String.class);` + === Altering the test class When creating a custom `QuarkusTestResourceLifecycleManager` that needs to inject the something into the test class, the `inject` methods can be used. From 6212935e625bc6f3b9df86f395282b66f627a95e Mon Sep 17 00:00:00 2001 From: Peter Palaga Date: Mon, 25 Dec 2023 21:59:26 +0100 Subject: [PATCH 06/36] Reproduce #37934 Failed to find class com.fasterxml.jackson.databind.ext.CoreXMLSerializers when a POJO has field of type javax.xml.datatype.XMLGregorianCalendar --- .../quarkus/it/resteasy/jackson/Greeting.java | 10 ++++++- .../it/resteasy/jackson/GreetingResource.java | 11 +++---- .../jackson/GreetingResourceTest.java | 30 ++++++++++++++++--- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/integration-tests/resteasy-jackson/src/main/java/io/quarkus/it/resteasy/jackson/Greeting.java b/integration-tests/resteasy-jackson/src/main/java/io/quarkus/it/resteasy/jackson/Greeting.java index 408e80854c6e7..986165aea3bd3 100644 --- a/integration-tests/resteasy-jackson/src/main/java/io/quarkus/it/resteasy/jackson/Greeting.java +++ b/integration-tests/resteasy-jackson/src/main/java/io/quarkus/it/resteasy/jackson/Greeting.java @@ -3,16 +3,20 @@ import java.sql.Date; import java.time.LocalDate; +import javax.xml.datatype.XMLGregorianCalendar; + public class Greeting { private final String message; private final LocalDate date; private final Date sqlDate; + private final XMLGregorianCalendar xmlGregorianCalendar; - public Greeting(String message, LocalDate date, Date sqlDate) { + public Greeting(String message, LocalDate date, Date sqlDate, XMLGregorianCalendar xmlGregorianCalendar) { this.message = message; this.date = date; this.sqlDate = sqlDate; + this.xmlGregorianCalendar = xmlGregorianCalendar; } public String getMessage() { @@ -26,4 +30,8 @@ public LocalDate getDate() { public Date getSqlDate() { return sqlDate; } + + public XMLGregorianCalendar getXmlGregorianCalendar() { + return xmlGregorianCalendar; + } } diff --git a/integration-tests/resteasy-jackson/src/main/java/io/quarkus/it/resteasy/jackson/GreetingResource.java b/integration-tests/resteasy-jackson/src/main/java/io/quarkus/it/resteasy/jackson/GreetingResource.java index 86be0fc37d46d..633234fee55ad 100644 --- a/integration-tests/resteasy-jackson/src/main/java/io/quarkus/it/resteasy/jackson/GreetingResource.java +++ b/integration-tests/resteasy-jackson/src/main/java/io/quarkus/it/resteasy/jackson/GreetingResource.java @@ -1,9 +1,7 @@ package io.quarkus.it.resteasy.jackson; -import java.sql.Date; -import java.time.LocalDate; - 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; @@ -13,10 +11,9 @@ @Path("/greeting") public class GreetingResource { - @GET - public Greeting hello() { - LocalDate localDate = LocalDate.of(2019, 01, 01); - return new Greeting("hello", localDate, new Date(localDate.toEpochDay())); + @POST + public Greeting hello(Greeting body) { + return body; } @Path("config") diff --git a/integration-tests/resteasy-jackson/src/test/java/io/quarkus/it/resteasy/jackson/GreetingResourceTest.java b/integration-tests/resteasy-jackson/src/test/java/io/quarkus/it/resteasy/jackson/GreetingResourceTest.java index 5cc175abf9292..a23637ffc4b63 100644 --- a/integration-tests/resteasy-jackson/src/test/java/io/quarkus/it/resteasy/jackson/GreetingResourceTest.java +++ b/integration-tests/resteasy-jackson/src/test/java/io/quarkus/it/resteasy/jackson/GreetingResourceTest.java @@ -2,22 +2,44 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; + +import java.sql.Date; +import java.time.LocalDate; +import java.time.ZoneOffset; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; @QuarkusTest class GreetingResourceTest { @Test - void testEndpoint() { + void testEndpoint() throws DatatypeConfigurationException { + + final LocalDate localDate = LocalDate.of(2019, 01, 01); + final Date sqlDate = new Date(localDate.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()); + final XMLGregorianCalendar xmlGregorianCalendar = DatatypeFactory.newInstance() + .newXMLGregorianCalendar("2019-01-01T00:00:00.000+00:00"); + final Greeting greeting = new Greeting("hello", localDate, sqlDate, xmlGregorianCalendar); + given() - .when().get("/greeting") + .contentType(ContentType.JSON) + .body(greeting) + .post("/greeting") .then() .statusCode(200) - .body(containsString("hello")) - .body(containsString("2019-01-01")); + .body("message", equalTo("hello"), + "date", equalTo("2019-01-01"), + "sqlDate", equalTo("2019-01-01"), + "xmlGregorianCalendar", equalTo("2019-01-01T00:00:00.000+00:00")) + .extract().body().asString(); } @Test From 77dde4e20ea2bcc544e6cc0e7bf40f636122a3a5 Mon Sep 17 00:00:00 2001 From: Peter Palaga Date: Mon, 25 Dec 2023 22:30:09 +0100 Subject: [PATCH 07/36] Failed to find class com.fasterxml.jackson.databind.ext.CoreXMLSerializers when a POJO has field of type javax.xml.datatype.XMLGregorianCalendar fix #37934 --- .../io/quarkus/jackson/deployment/JacksonProcessor.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java index dc7b382a921a4..0560f937fce55 100644 --- a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java +++ b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java @@ -125,8 +125,13 @@ void register( "com.fasterxml.jackson.databind.deser.std.DateDeserializers$SqlDateDeserializer", "com.fasterxml.jackson.databind.deser.std.DateDeserializers$TimestampDeserializer", "com.fasterxml.jackson.annotation.SimpleObjectIdResolver").methods().build()); - reflectiveClass.produce(ReflectiveClassBuildItem.builder("com.fasterxml.jackson.databind.ser.std.ClassSerializer") - .constructors().build()); + reflectiveClass.produce( + ReflectiveClassBuildItem.builder( + "com.fasterxml.jackson.databind.ser.std.ClassSerializer", + "com.fasterxml.jackson.databind.ext.CoreXMLSerializers", + "com.fasterxml.jackson.databind.ext.CoreXMLDeserializers") + .constructors() + .build()); if (curateOutcomeBuildItem.getApplicationModel().getDependencies().stream().anyMatch( x -> x.getGroupId().equals("com.fasterxml.jackson.module") From a40446b73a8b42fe2bd8cae210ba24a3311ad7cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 22:41:02 +0000 Subject: [PATCH 08/36] Bump com.google.errorprone:error_prone_annotations from 2.23.0 to 2.24.0 Bumps [com.google.errorprone:error_prone_annotations](https://github.com/google/error-prone) from 2.23.0 to 2.24.0. - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.23.0...v2.24.0) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_annotations dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 5f261e167e978..113d7bf2b6ae2 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -194,7 +194,7 @@ 23.0.1 1.15.1 3.42.0 - 2.23.0 + 2.24.0 0.25.0 1.43.3 2.1 From bb89fe365ebc822b3b4de78858bfde5208087236 Mon Sep 17 00:00:00 2001 From: Jakub Scholz Date: Thu, 28 Dec 2023 17:06:56 +0100 Subject: [PATCH 09/36] Update Strimzi container images in docs Signed-off-by: Jakub Scholz Signed-off-by: Jakub Scholz --- docs/src/main/asciidoc/kafka-dev-services.adoc | 2 +- .../src/main/asciidoc/kafka-reactive-getting-started.adoc | 4 ++-- docs/src/main/asciidoc/kafka-schema-registry-avro.adoc | 8 ++++---- .../main/asciidoc/kafka-schema-registry-json-schema.adoc | 8 ++++---- docs/src/main/asciidoc/kafka-streams.adoc | 4 ++-- docs/src/main/asciidoc/kafka.adoc | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index a94da965a3a21..2c0a35c674c8c 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -82,7 +82,7 @@ For Strimzi, you can select any image with a Kafka version which has Kraft suppo [source, properties] ---- -quarkus.kafka.devservices.image-name=quay.io/strimzi-test-container/test-container:0.100.0-kafka-3.1.0 +quarkus.kafka.devservices.image-name=quay.io/strimzi-test-container/test-container:0.105.0-kafka-3.6.0 ---- == Configuring Kafka topics diff --git a/docs/src/main/asciidoc/kafka-reactive-getting-started.adoc b/docs/src/main/asciidoc/kafka-reactive-getting-started.adoc index 0cda3a44255a1..dddebeb5a07c1 100644 --- a/docs/src/main/asciidoc/kafka-reactive-getting-started.adoc +++ b/docs/src/main/asciidoc/kafka-reactive-getting-started.adoc @@ -408,7 +408,7 @@ version: '3.5' services: zookeeper: - image: quay.io/strimzi/kafka:0.23.0-kafka-2.8.0 + image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 command: [ "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" @@ -421,7 +421,7 @@ services: - kafka-quickstart-network kafka: - image: quay.io/strimzi/kafka:0.23.0-kafka-2.8.0 + image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 command: [ "sh", "-c", "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" diff --git a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc index ca1b4c787a759..224a81fbe682e 100644 --- a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc +++ b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc @@ -324,7 +324,7 @@ version: '2' services: zookeeper: - image: quay.io/strimzi/kafka:0.22.1-kafka-2.7.0 + image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 command: [ "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" @@ -335,7 +335,7 @@ services: LOG_DIR: /tmp/logs kafka: - image: quay.io/strimzi/kafka:0.22.1-kafka-2.7.0 + image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 command: [ "sh", "-c", "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" @@ -545,7 +545,7 @@ If we couldn't use Dev Services and wanted to start a Kafka broker and Apicurio io.strimzi strimzi-test-container - 0.22.1 + 0.105.0 test @@ -559,7 +559,7 @@ If we couldn't use Dev Services and wanted to start a Kafka broker and Apicurio [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -testImplementation("io.strimzi:strimzi-test-container:0.22.1") { +testImplementation("io.strimzi:strimzi-test-container:0.105.0") { exclude group: "org.apache.logging.log4j", module: "log4j-core" } ---- diff --git a/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc b/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc index ac3270f611662..3a4308a8e3b91 100644 --- a/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc +++ b/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc @@ -352,7 +352,7 @@ version: '2' services: zookeeper: - image: quay.io/strimzi/kafka:0.22.1-kafka-2.7.0 + image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 command: [ "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" @@ -363,7 +363,7 @@ services: LOG_DIR: /tmp/logs kafka: - image: quay.io/strimzi/kafka:0.22.1-kafka-2.7.0 + image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 command: [ "sh", "-c", "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" @@ -573,7 +573,7 @@ If we couldn't use Dev Services and wanted to start a Kafka broker and Apicurio io.strimzi strimzi-test-container - 0.22.1 + 0.105.0 test @@ -587,7 +587,7 @@ If we couldn't use Dev Services and wanted to start a Kafka broker and Apicurio [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -testImplementation("io.strimzi:strimzi-test-container:0.22.1") { +testImplementation("io.strimzi:strimzi-test-container:0.105.0") { exclude group: "org.apache.logging.log4j", module: "log4j-core" } ---- diff --git a/docs/src/main/asciidoc/kafka-streams.adoc b/docs/src/main/asciidoc/kafka-streams.adoc index e5cc5fa70d8fa..28f940e4a9e3a 100644 --- a/docs/src/main/asciidoc/kafka-streams.adoc +++ b/docs/src/main/asciidoc/kafka-streams.adoc @@ -499,7 +499,7 @@ version: '3.5' services: zookeeper: - image: strimzi/kafka:0.19.0-kafka-2.5.0 + image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 command: [ "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" @@ -511,7 +511,7 @@ services: networks: - kafkastreams-network kafka: - image: strimzi/kafka:0.19.0-kafka-2.5.0 + image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 command: [ "sh", "-c", "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT} --override num.partitions=$${KAFKA_NUM_PARTITIONS}" diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index c6487f654f687..7631552643070 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -2274,7 +2274,7 @@ The configuration of the created Kafka broker can be customized using `@Resource [source,java] ---- @QuarkusTestResource(value = KafkaCompanionResource.class, initArgs = { - @ResourceArg(name = "strimzi.kafka.image", value = "quay.io/strimzi/kafka:0.28.0-kafka-3.0.0"), // Image name + @ResourceArg(name = "strimzi.kafka.image", value = "quay.io/strimzi-test-container/test-container:0.105.0-kafka-3.6.0"), // Image name @ResourceArg(name = "kafka.port", value = "9092"), // Fixed port for kafka, by default it will be exposed on a random port @ResourceArg(name = "kraft", value = "true"), // Enable Kraft mode @ResourceArg(name = "num.partitions", value = "3"), // Other custom broker configurations From 63e5320a5be516df36167f689366ebbd03db598d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 20:28:05 +0000 Subject: [PATCH 10/36] Bump org.assertj:assertj-core from 3.24.2 to 3.25.0 in /devtools/gradle Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.24.2 to 3.25.0. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.24.2...assertj-build-3.25.0) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- devtools/gradle/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index 5bb10bc0b84fc..768b863cd27d6 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -6,7 +6,7 @@ kotlin = "1.9.21" smallrye-config = "3.4.4" junit5 = "5.10.1" -assertj = "3.24.2" +assertj = "3.25.0" [plugins] plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "plugin-publish" } From 17d31875061cc30aa2d00e10213bf64dc314adbc Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 2 Jan 2024 11:06:32 +0000 Subject: [PATCH 11/36] Add test for EnumConverter --- .../HyphenateEnumConverterTest.java | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/core/runtime/src/test/java/io/quarkus/runtime/configuration/HyphenateEnumConverterTest.java b/core/runtime/src/test/java/io/quarkus/runtime/configuration/HyphenateEnumConverterTest.java index d9c8e0255a9f8..907c109aab172 100644 --- a/core/runtime/src/test/java/io/quarkus/runtime/configuration/HyphenateEnumConverterTest.java +++ b/core/runtime/src/test/java/io/quarkus/runtime/configuration/HyphenateEnumConverterTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; -public class HyphenateEnumConverterTest { +class HyphenateEnumConverterTest { enum MyEnum { DISCARD, READ_UNCOMMITTED, @@ -20,53 +20,71 @@ enum MyOtherEnum { READ__UNCOMMITTED } - HyphenateEnumConverter hyphenateEnumConverter; - @Test - public void convertMyEnum() { - hyphenateEnumConverter = new HyphenateEnumConverter(MyEnum.class); - MyEnum myEnum = (MyEnum) hyphenateEnumConverter.convert("DISCARD"); + void convertMyEnum() { + HyphenateEnumConverter converter = new HyphenateEnumConverter<>(MyEnum.class); + MyEnum myEnum = converter.convert("DISCARD"); assertEquals(myEnum, MyEnum.DISCARD); - myEnum = (MyEnum) hyphenateEnumConverter.convert("discard"); + myEnum = converter.convert("discard"); assertEquals(myEnum, MyEnum.DISCARD); - myEnum = (MyEnum) hyphenateEnumConverter.convert("READ_UNCOMMITTED"); + myEnum = converter.convert("READ_UNCOMMITTED"); assertEquals(myEnum, MyEnum.READ_UNCOMMITTED); - myEnum = (MyEnum) hyphenateEnumConverter.convert("read-uncommitted"); + myEnum = converter.convert("read-uncommitted"); assertEquals(myEnum, MyEnum.READ_UNCOMMITTED); - myEnum = (MyEnum) hyphenateEnumConverter.convert("SIGUSR1"); + myEnum = converter.convert("SIGUSR1"); assertEquals(myEnum, MyEnum.SIGUSR1); - myEnum = (MyEnum) hyphenateEnumConverter.convert("sigusr1"); + myEnum = converter.convert("sigusr1"); assertEquals(myEnum, MyEnum.SIGUSR1); - myEnum = (MyEnum) hyphenateEnumConverter.convert("TrendBreaker"); + myEnum = converter.convert("TrendBreaker"); assertEquals(myEnum, MyEnum.TrendBreaker); - myEnum = (MyEnum) hyphenateEnumConverter.convert("trend-breaker"); + myEnum = converter.convert("trend-breaker"); assertEquals(myEnum, MyEnum.TrendBreaker); - myEnum = (MyEnum) hyphenateEnumConverter.convert("MAKING_LifeDifficult"); + myEnum = converter.convert("MAKING_LifeDifficult"); assertEquals(myEnum, MyEnum.MAKING_LifeDifficult); - myEnum = (MyEnum) hyphenateEnumConverter.convert("making-life-difficult"); + myEnum = converter.convert("making-life-difficult"); assertEquals(myEnum, MyEnum.MAKING_LifeDifficult); - myEnum = (MyEnum) hyphenateEnumConverter.convert("YeOldeJBoss"); + myEnum = converter.convert("YeOldeJBoss"); assertEquals(myEnum, MyEnum.YeOldeJBoss); - myEnum = (MyEnum) hyphenateEnumConverter.convert("ye-olde-jboss"); + myEnum = converter.convert("ye-olde-jboss"); assertEquals(myEnum, MyEnum.YeOldeJBoss); } @Test - public void convertMyOtherEnum() { - hyphenateEnumConverter = new HyphenateEnumConverter(MyOtherEnum.class); - MyOtherEnum myOtherEnum = (MyOtherEnum) hyphenateEnumConverter.convert("makingLifeDifficult"); + void convertMyOtherEnum() { + HyphenateEnumConverter converter = new HyphenateEnumConverter<>(MyOtherEnum.class); + MyOtherEnum myOtherEnum = converter.convert("makingLifeDifficult"); assertEquals(myOtherEnum, MyOtherEnum.makingLifeDifficult); - myOtherEnum = (MyOtherEnum) hyphenateEnumConverter.convert("making-life-difficult"); + myOtherEnum = converter.convert("making-life-difficult"); assertEquals(myOtherEnum, MyOtherEnum.makingLifeDifficult); - myOtherEnum = (MyOtherEnum) hyphenateEnumConverter.convert("READ__UNCOMMITTED"); + myOtherEnum = converter.convert("READ__UNCOMMITTED"); assertEquals(myOtherEnum, MyOtherEnum.READ__UNCOMMITTED); - myOtherEnum = (MyOtherEnum) hyphenateEnumConverter.convert("read-uncommitted"); + myOtherEnum = converter.convert("read-uncommitted"); assertEquals(myOtherEnum, MyOtherEnum.READ__UNCOMMITTED); } @Test - public void testIllegalEnumConfigUtilConversion() { - hyphenateEnumConverter = new HyphenateEnumConverter(MyEnum.class); + void illegalEnumConfigUtilConversion() { + HyphenateEnumConverter hyphenateEnumConverter = new HyphenateEnumConverter<>(MyEnum.class); assertThrows(IllegalArgumentException.class, () -> hyphenateEnumConverter.convert("READUNCOMMITTED")); } + + enum HTTPConduit { + QuarkusCXFDefault, + CXFDefault, + HttpClientHTTPConduitFactory, + URLConnectionHTTPConduitFactory + } + + @Test + void convertHttpConduit() { + HyphenateEnumConverter converter = new HyphenateEnumConverter<>(HTTPConduit.class); + assertEquals(HTTPConduit.QuarkusCXFDefault, converter.convert("QuarkusCXFDefault")); + assertEquals(HTTPConduit.QuarkusCXFDefault, converter.convert("quarkus-cxf-default")); + assertEquals(HTTPConduit.CXFDefault, converter.convert("CXFDefault")); + assertEquals(HTTPConduit.CXFDefault, converter.convert("cxf-default")); + assertEquals(HTTPConduit.HttpClientHTTPConduitFactory, converter.convert("HttpClientHTTPConduitFactory")); + assertEquals(HTTPConduit.HttpClientHTTPConduitFactory, converter.convert("http-client-http-conduit-factory")); + assertEquals(HTTPConduit.URLConnectionHTTPConduitFactory, converter.convert("URLConnectionHTTPConduitFactory")); + assertEquals(HTTPConduit.URLConnectionHTTPConduitFactory, converter.convert("url-connection-http-conduit-factory")); + } } From 5545a2b081be6cd476341ea3d2a2f6b36b3f5003 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 2 Jan 2024 11:20:26 +0000 Subject: [PATCH 12/36] Fix Create the Maven project section in security-oidc-bearer-token-authentication-tutorial.adoc --- ...-bearer-token-authentication-tutorial.adoc | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc index 6b26fa8d86dbf..a9bd55ff4289a 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc @@ -57,47 +57,33 @@ The solution is located in the `security-openid-connect-quickstart` link:{quicks You can either create a new Maven project with the `oidc` extension or you can add the extension to an existing Maven project. Complete one of the following commands: -* To create a new Maven project, use the following command: -+ -==== +To create a new Maven project, use the following command: + :create-app-artifact-id: security-openid-connect-quickstart :create-app-extensions: oidc,resteasy-reactive-jackson include::{includes}/devtools/create-app.adoc[] -==== -This command generates a Maven project, importing the `oidc` extension -which is an implementation of OIDC for Quarkus. -* If you already have your Quarkus project configured, you can add the `oidc` extension -to your project by running the following command in your project base directory: -+ -==== +If you already have your Quarkus project configured, you can add the `oidc` extension to your project by running the following command in your project base directory: + :add-extension-extensions: oidc include::{includes}/devtools/extension-add.adoc[] -==== -The following configuration gets added to your build file: -* Using Maven (pom.xml): -+ -==== +This will add the following to your build file: + [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml ---- - io.quarkus - quarkus-oidc + io.quarkus + quarkus-oidc ---- -==== -+ -* Using Gradle (build.gradle): -+ -==== --- + [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle ---- implementation("io.quarkus:quarkus-oidc") ---- --- -==== == Write the application From c10eea9fe0997e91cc964bdbe51667615d7cf34c Mon Sep 17 00:00:00 2001 From: Tamaro Skaljic <49238587+tamaro-skaljic@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:29:15 +0100 Subject: [PATCH 13/36] Add documentation for pushing with Podman to Heroku --- .../main/asciidoc/deploying-to-heroku.adoc | 74 ++++++++++++++----- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/docs/src/main/asciidoc/deploying-to-heroku.adoc b/docs/src/main/asciidoc/deploying-to-heroku.adoc index 97b717a81b25d..bfc76f7dc0d27 100644 --- a/docs/src/main/asciidoc/deploying-to-heroku.adoc +++ b/docs/src/main/asciidoc/deploying-to-heroku.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: cloud :summary: Deploy your Quarkus applications on Heroku. -:topics: devops,heroku,cloud,deployment +:topics: devops,heroku,cloud,deployment,docker,podman In this guide you will learn how to deploy a Quarkus based web application as a web-dyno to Heroku. @@ -16,8 +16,10 @@ This guide covers: * Update Quarkus HTTP Port * Install the Heroku CLI * Deploy the application to Heroku -* Deploy the application as Docker image to Heroku -* Deploy the native application as Docker image to Heroku +* Deploy the application as container image to Heroku + * Using Docker + * Using Podman +* Deploy the native application as container image to Heroku == Prerequisites @@ -42,8 +44,6 @@ Heroku can be used in different ways to run a Quarkus application: All three approaches need to be aware of the port that Heroku assigns to it to handle traffic. Luckily, there's a dynamic configuration property for it. -The guide assumes that you have the https://devcenter.heroku.com/articles/heroku-cli[Heroku CLI] installed. - == Common project setup This guide will take as input an application developed in the xref:getting-started.adoc[Getting Started guide]. @@ -82,12 +82,12 @@ echo "quarkus.http.port=\${PORT:8080}" >> src/main/resources/application.propert git commit -am "Configure the HTTP Port." ---- -== Deploy the repository and build on Heroku +== Push the repository to Heroku The first variant uses the Quarkus Maven build to create the _quarkus-app_ application structure containing the runnable "fast-jar" as well as all libraries needed inside Heroku's build infrastructure and then deploying that result, the other one uses a local build process to create an optimized container. -Two additional files are needed in your application's root directory: +For the first variant, two additional files are needed in your application's root directory: * `system.properties` to configure the Java version * `Procfile` to configure how Heroku starts your application @@ -128,7 +128,7 @@ To access the REST endpoint via curl, run: [source,bash] ---- APP_NAME=`heroku info | grep "=== .*" |sed "s/=== //"` -curl $APP_NAME.herokuapp.com/hello +curl $APP_NAME.herokuapp.com/hello?name=World ---- Of course, you can use the Heroku CLI to connect this repo to your GitHub account, too, but this is out of scope for this guide. @@ -154,7 +154,7 @@ git commit -am "Add container-image-docker extension." ---- The image we are going to build needs to be named accordingly to work with Heroku's registry and deployment. -We get the generated name via `heroku info` and pass it on to the (local) build: +We get the generated name via `heroku info` and pass it on to the (local) build. [source,bash] ---- @@ -166,6 +166,18 @@ mvn clean package\ -Dquarkus.container-image.tag=latest ---- +== Push and release the image + +You can now push the image and release it. + +[NOTE] +==== +The initial push is rather big, as all layers of the image need to be transferred. +The following pushes will be smaller. +==== + +=== Pushing through Docker + With Docker installed, you can now push the image and release it: [source,bash] @@ -174,6 +186,39 @@ docker push registry.heroku.com/$APP_NAME/web heroku container:release web --app $APP_NAME ---- +=== Pushing through Podman + +When you want to use Podman as a drop-in-replacement for Docker, you will have some problems because the Heroku CLI depends on Docker. But there are possible solutions for these problems. + +[IMPORTANT] +.Cannot find docker, please ensure docker is installed. +==== +The problem is obviously that the heroku-cli can’t find docker. This is quite easy to resolve, because the podman cli is docker-compatible. We just need to create a symlink from podman to docker: +[source,bash] +---- +sudo ln -s $(which podman) /usr/local/bin/docker +---- +==== + +[IMPORTANT] +.Error writing manifest: Error uploading manifest latest to registry.heroku.com/$APP_NAME/web: unsupported +==== +Instead of doing a normal podman push (OCI format) we must use a workaround in order to push and release our app through Podman and the Heroku CLI in the desired format (v2s2 - Docker Image Manifest Version 2, Schema 2). +[source,bash] +---- +CONTAINER_DIR="target/container-dir" +mkdir $CONTAINER_DIR +podman push --format=v2s2 "registry.heroku.com/$APP_NAME/web" dir:$CONTAINER_DIR +skopeo --debug copy dir:$CONTAINER_DIR "docker://registry.heroku.com/$APP_NAME/web:latest" +heroku container:release web --app "$APP_NAME" +rm -rf $CONTAINER_DIR +---- +==== + +* https://urhengulas.github.io/blog/podman_heroku.html[Source of solutions and workarounds] + +== Check the logs + You can and should check the logs to see if your application is now indeed running from the container: [source,bash] @@ -181,8 +226,7 @@ You can and should check the logs to see if your application is now indeed runni heroku logs --app $APP_NAME --tail ---- -The initial push is rather big, as all layers of the image need to be transferred. -The following pushes will be smaller. +== Deploy as native application inside a container The biggest advantage we take when deploying our app as a container is to deploy a container with the natively compiled application. Why? Because Heroku will stop or sleep the application when there's no incoming traffic. @@ -203,10 +247,4 @@ mvn clean package\ -Dquarkus.native.container-build=true ---- -After that, push and release again: - -[source,bash] ----- -docker push registry.heroku.com/$APP_NAME/web -heroku container:release web --app $APP_NAME ----- +After that, push and release again (see above). From fd358d33bb8f3933b2d716f87b610a7ccd00a29d Mon Sep 17 00:00:00 2001 From: Stephan Strate Date: Tue, 2 Jan 2024 11:35:34 +0100 Subject: [PATCH 14/36] Expose pandaproxy port in Redpanda dev services --- .../src/main/asciidoc/kafka-dev-services.adoc | 4 ++-- .../deployment/DevServicesKafkaProcessor.java | 4 ++-- .../KafkaDevServicesBuildTimeConfig.java | 6 +++--- ...nfig.java => RedpandaBuildTimeConfig.java} | 20 ++++++++++++++----- ...ainer.java => RedpandaKafkaContainer.java} | 18 ++++++++++++----- 5 files changed, 35 insertions(+), 17 deletions(-) rename extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/{RedPandaBuildTimeConfig.java => RedpandaBuildTimeConfig.java} (58%) rename extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/{RedPandaKafkaContainer.java => RedpandaKafkaContainer.java} (86%) diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index a94da965a3a21..f27baa5064896 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -14,7 +14,7 @@ If any Kafka-related extension is present (e.g. `quarkus-smallrye-reactive-messa So, you don't have to start a broker manually. The application is configured automatically. -IMPORTANT: Because starting a Kafka broker can be long, Dev Services for Kafka uses https://vectorized.io/redpanda[Redpanda], a Kafka compatible broker which starts in ~1 second. +IMPORTANT: Because starting a Kafka broker can be long, Dev Services for Kafka uses https://redpanda.com[Redpanda], a Kafka compatible broker which starts in ~1 second. == Enabling / Disabling Dev Services for Kafka @@ -106,7 +106,7 @@ You can configure timeout for Kafka admin client calls used in topic creation us [[redpanda-transactions]] == Transactional and Idempotent producers support -By default, the Red Panda broker is configured to enable transactions and idempotence features. +By default, the Redpanda broker is configured to enable transactions and idempotence features. You can disable those using: [source, properties] diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java index e6d20a2e7e7df..312628e10b9ac 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java @@ -227,7 +227,7 @@ private RunningDevService startKafka(DockerStatusBuildItem dockerStatusBuildItem final Supplier defaultKafkaBrokerSupplier = () -> { switch (config.provider) { case REDPANDA: - RedPandaKafkaContainer redpanda = new RedPandaKafkaContainer( + RedpandaKafkaContainer redpanda = new RedpandaKafkaContainer( DockerImageName.parse(config.imageName).asCompatibleSubstituteFor("vectorized/redpanda"), config.fixedExposedPort, launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT ? config.serviceName : null, @@ -321,7 +321,7 @@ private static final class KafkaDevServiceCfg { private final KafkaDevServicesBuildTimeConfig.Provider provider; - private final RedPandaBuildTimeConfig redpanda; + private final RedpandaBuildTimeConfig redpanda; public KafkaDevServiceCfg(KafkaDevServicesBuildTimeConfig config) { this.devServicesEnabled = config.enabled.orElse(true); diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java index 5c909c99a674a..756662efdd317 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java @@ -33,7 +33,7 @@ public class KafkaDevServicesBuildTimeConfig { * Redpanda, Strimzi and kafka-native container providers are supported. Default is redpanda. *

* For Redpanda: - * See https://vectorized.io/docs/quick-start-docker/ and https://hub.docker.com/r/vectorized/redpanda + * See https://docs.redpanda.com/current/get-started/quick-start/ and https://hub.docker.com/r/vectorized/redpanda *

* For Strimzi: * See https://github.com/strimzi/test-container and https://quay.io/repository/strimzi-test-container/test-container @@ -123,9 +123,9 @@ public String getDefaultImageName() { public Map containerEnv; /** - * Allows configuring the Red Panda broker. + * Allows configuring the Redpanda broker. */ @ConfigItem - public RedPandaBuildTimeConfig redpanda; + public RedpandaBuildTimeConfig redpanda; } diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedPandaBuildTimeConfig.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedpandaBuildTimeConfig.java similarity index 58% rename from extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedPandaBuildTimeConfig.java rename to extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedpandaBuildTimeConfig.java index f8e7ebfbdec03..ab9d1345f8633 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedPandaBuildTimeConfig.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedpandaBuildTimeConfig.java @@ -1,22 +1,24 @@ package io.quarkus.kafka.client.deployment; +import java.util.Optional; + import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; /** - * Allows configuring the Red Panda broker. - * Notice that Red Panda is not a "genuine" Kafka, it's a 100% compatible implementation of the protocol. + * Allows configuring the Redpanda broker. + * Notice that Redpanda is not a "genuine" Kafka, it's a 100% compatible implementation of the protocol. * - * Find more info about Red Panda on https://vectorized.io/redpanda/. + * Find more info about Redpanda on https://redpanda.com/. */ @ConfigGroup -public class RedPandaBuildTimeConfig { +public class RedpandaBuildTimeConfig { /** * Enables transaction support. * Also enables the producer idempotence. * - * Find more info about Red Panda transaction support on + * Find more info about Redpanda transaction support on * https://vectorized.io/blog/fast-transactions/. * * Notice that @@ -28,4 +30,12 @@ public class RedPandaBuildTimeConfig { */ @ConfigItem(defaultValue = "true") public boolean transactionEnabled; + + /** + * Port to access the Redpanda HTTP Proxy (pandaproxy). + *

+ * If not defined, the port will be chosen randomly. + */ + @ConfigItem + public Optional proxyPort; } diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedPandaKafkaContainer.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedpandaKafkaContainer.java similarity index 86% rename from extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedPandaKafkaContainer.java rename to extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedpandaKafkaContainer.java index e885b936d3b11..8dca5de6d1c12 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedPandaKafkaContainer.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/RedpandaKafkaContainer.java @@ -15,20 +15,22 @@ /** * Container configuring and starting the Redpanda broker. - * See https://vectorized.io/docs/quick-start-docker/ + * See https://docs.redpanda.com/current/get-started/quick-start/ */ -final class RedPandaKafkaContainer extends GenericContainer { +final class RedpandaKafkaContainer extends GenericContainer { private final Integer fixedExposedPort; private final boolean useSharedNetwork; - private final RedPandaBuildTimeConfig redpandaConfig; + private final RedpandaBuildTimeConfig redpandaConfig; private String hostName = null; private static final String STARTER_SCRIPT = "/var/lib/redpanda/redpanda.sh"; + private static final int PANDAPROXY_PORT = 8082; - RedPandaKafkaContainer(DockerImageName dockerImageName, int fixedExposedPort, String serviceName, - boolean useSharedNetwork, RedPandaBuildTimeConfig redpandaConfig) { + RedpandaKafkaContainer(DockerImageName dockerImageName, int fixedExposedPort, String serviceName, + boolean useSharedNetwork, RedpandaBuildTimeConfig redpandaConfig) { super(dockerImageName); this.fixedExposedPort = fixedExposedPort; this.useSharedNetwork = useSharedNetwork; @@ -101,6 +103,12 @@ protected void configure() { if (fixedExposedPort != null) { addFixedExposedPort(fixedExposedPort, DevServicesKafkaProcessor.KAFKA_PORT); } + + if (redpandaConfig.proxyPort.isPresent()) { + addFixedExposedPort(redpandaConfig.proxyPort.get(), PANDAPROXY_PORT); + } else { + addExposedPort(PANDAPROXY_PORT); + } } public String getBootstrapServers() { From c25bfc974108330ee7d7e494756e60e63eb5bc79 Mon Sep 17 00:00:00 2001 From: Tamaro Skaljic <49238587+tamaro-skaljic@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:36:52 +0100 Subject: [PATCH 15/36] Cleanup commit --- docs/src/main/asciidoc/deploying-to-heroku.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/deploying-to-heroku.adoc b/docs/src/main/asciidoc/deploying-to-heroku.adoc index bfc76f7dc0d27..8d50e5cb493a3 100644 --- a/docs/src/main/asciidoc/deploying-to-heroku.adoc +++ b/docs/src/main/asciidoc/deploying-to-heroku.adoc @@ -82,7 +82,7 @@ echo "quarkus.http.port=\${PORT:8080}" >> src/main/resources/application.propert git commit -am "Configure the HTTP Port." ---- -== Push the repository to Heroku +== Deploy the repository and build on Heroku The first variant uses the Quarkus Maven build to create the _quarkus-app_ application structure containing the runnable "fast-jar" as well as all libraries needed inside Heroku's build infrastructure and then deploying that result, the other one uses a local build process to create an optimized container. @@ -128,7 +128,7 @@ To access the REST endpoint via curl, run: [source,bash] ---- APP_NAME=`heroku info | grep "=== .*" |sed "s/=== //"` -curl $APP_NAME.herokuapp.com/hello?name=World +curl $APP_NAME.herokuapp.com/hello ---- Of course, you can use the Heroku CLI to connect this repo to your GitHub account, too, but this is out of scope for this guide. @@ -154,7 +154,7 @@ git commit -am "Add container-image-docker extension." ---- The image we are going to build needs to be named accordingly to work with Heroku's registry and deployment. -We get the generated name via `heroku info` and pass it on to the (local) build. +We get the generated name via `heroku info` and pass it on to the (local) build: [source,bash] ---- @@ -178,7 +178,7 @@ The following pushes will be smaller. === Pushing through Docker -With Docker installed, you can now push the image and release it: +With Docker installed, these steps are simple: [source,bash] ---- @@ -188,7 +188,7 @@ heroku container:release web --app $APP_NAME === Pushing through Podman -When you want to use Podman as a drop-in-replacement for Docker, you will have some problems because the Heroku CLI depends on Docker. But there are possible solutions for these problems. +When you want to use Podman as a drop-in-replacement for Docker, you will have some problems because the Heroku CLI depends on Docker and doesn't support the OCI format. But there are possible solutions for these problems. [IMPORTANT] .Cannot find docker, please ensure docker is installed. @@ -203,7 +203,7 @@ sudo ln -s $(which podman) /usr/local/bin/docker [IMPORTANT] .Error writing manifest: Error uploading manifest latest to registry.heroku.com/$APP_NAME/web: unsupported ==== -Instead of doing a normal podman push (OCI format) we must use a workaround in order to push and release our app through Podman and the Heroku CLI in the desired format (v2s2 - Docker Image Manifest Version 2, Schema 2). +Instead of doing a normal podman push (OCI format) we must use a workaround in order to push and release our app through Podman and the Heroku CLI in the desired format (v2s2 - Docker Image Manifest Version 2, Schema 2). Also https://github.com/containers/skopeo[skopeo] is needed. [source,bash] ---- CONTAINER_DIR="target/container-dir" @@ -247,4 +247,4 @@ mvn clean package\ -Dquarkus.native.container-build=true ---- -After that, push and release again (see above). +After that, push and release again using Docker or Podman (see above) and check the logs. From a90160da4da965434c16eddee119e0bb01545fcd Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Fri, 24 Nov 2023 10:30:50 +0100 Subject: [PATCH 16/36] Definition exception for multiple different emitter configurations for a channel --- .../SmallRyeReactiveMessagingProcessor.java | 17 +++- ...ChannelEmitterWithMultipleDefinitions.java | 86 +++++++++++++++++++ ...mitterWithMultipleInjectionPointsTest.java | 29 +++++++ .../runtime/QuarkusEmitterConfiguration.java | 35 ++++++++ 4 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/channels/ChannelEmitterWithMultipleDefinitions.java create mode 100644 extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/channels/EmitterWithMultipleInjectionPointsTest.java diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java index aa82b90d8ac8c..e4982848cd5a4 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java @@ -9,6 +9,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -234,7 +235,7 @@ public void build(SmallRyeReactiveMessagingRecorder recorder, RecorderContext re List mediatorConfigurations = new ArrayList<>(mediatorMethods.size()); List workerConfigurations = new ArrayList<>(); - List emittersConfigurations = new ArrayList<>(); + Map emittersConfigurations = new HashMap<>(); List channelConfigurations = new ArrayList<>(); /* @@ -293,15 +294,23 @@ public void build(SmallRyeReactiveMessagingRecorder recorder, RecorderContext re } for (InjectedEmitterBuildItem it : emitterFields) { - emittersConfigurations.add(it.getEmitterConfig()); + EmitterConfiguration configuration = it.getEmitterConfig(); + String channel = configuration.name(); + EmitterConfiguration previousConfig = emittersConfigurations.get(channel); + if (previousConfig != null && !previousConfig.equals(configuration)) { + throw new DeploymentException( + String.format("Emitter configuration for channel `%s` is different than previous configuration : %s", + channel, it.getEmitterConfig())); + } + emittersConfigurations.put(channel, configuration); } for (InjectedChannelBuildItem it : channelFields) { channelConfigurations.add(it.getChannelConfig()); } syntheticBeans.produce(SyntheticBeanBuildItem.configure(SmallRyeReactiveMessagingContext.class) - .supplier(recorder.createContext(mediatorConfigurations, workerConfigurations, emittersConfigurations, - channelConfigurations)) + .supplier(recorder.createContext(mediatorConfigurations, workerConfigurations, + new ArrayList<>(emittersConfigurations.values()), channelConfigurations)) .done()); } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/channels/ChannelEmitterWithMultipleDefinitions.java b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/channels/ChannelEmitterWithMultipleDefinitions.java new file mode 100644 index 0000000000000..d19cd482d2135 --- /dev/null +++ b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/channels/ChannelEmitterWithMultipleDefinitions.java @@ -0,0 +1,86 @@ +package io.quarkus.smallrye.reactivemessaging.channels; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.OnOverflow; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +@ApplicationScoped +public class ChannelEmitterWithMultipleDefinitions { + + @Inject + @Channel("sink") + @OnOverflow(value = OnOverflow.Strategy.BUFFER) + @Broadcast + Emitter emitter; + + private Emitter emitterForSink2; + + @Inject + public void setEmitter( + @Channel("sink") @Broadcast @OnOverflow(value = OnOverflow.Strategy.BUFFER, bufferSize = 4) Emitter sink2) { + this.emitterForSink2 = sink2; + } + + private final List list = new CopyOnWriteArrayList<>(); + private final List sink1 = new CopyOnWriteArrayList<>(); + private final List sink2 = new CopyOnWriteArrayList<>(); + + private final List list2 = new CopyOnWriteArrayList<>(); + private final List sink12 = new CopyOnWriteArrayList<>(); + private final List sink22 = new CopyOnWriteArrayList<>(); + + public void run() { + emitter.send("a"); + emitter.send("b"); + emitter.send("c").toCompletableFuture().join(); + emitter.complete(); + emitterForSink2.send("a2").toCompletableFuture().join(); + emitterForSink2.send("b2"); + emitterForSink2.send("c2"); + emitterForSink2.complete(); + } + + @Incoming("sink") + public void consume1(String s) { + list.add(s); + } + + @Incoming("sink") + public void consume2(String s) { + list2.add(s); + } + + public List list() { + return list; + } + + public List list2() { + return list2; + } + + public List sink11() { + return sink1; + } + + public List sink12() { + return sink12; + } + + public List sink21() { + return sink2; + } + + public List sink22() { + return sink22; + } + +} diff --git a/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/channels/EmitterWithMultipleInjectionPointsTest.java b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/channels/EmitterWithMultipleInjectionPointsTest.java new file mode 100644 index 0000000000000..b572d4a82efe5 --- /dev/null +++ b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/channels/EmitterWithMultipleInjectionPointsTest.java @@ -0,0 +1,29 @@ +package io.quarkus.smallrye.reactivemessaging.channels; + +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class EmitterWithMultipleInjectionPointsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ChannelEmitterWithMultipleDefinitions.class)) + .setExpectedException(DeploymentException.class); + + @Inject + ChannelEmitterWithOverflow bean; + + @Test + public void testEmitter() { + fail(); + } + +} diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/QuarkusEmitterConfiguration.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/QuarkusEmitterConfiguration.java index 2b33e70c7f3b9..bb670de2faabe 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/QuarkusEmitterConfiguration.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/QuarkusEmitterConfiguration.java @@ -1,5 +1,7 @@ package io.quarkus.smallrye.reactivemessaging.runtime; +import java.util.Objects; + import org.eclipse.microprofile.reactive.messaging.OnOverflow; import io.smallrye.reactive.messaging.EmitterConfiguration; @@ -115,4 +117,37 @@ public int getNumberOfSubscriberBeforeConnecting() { public void setNumberOfSubscriberBeforeConnecting(int numberOfSubscriberBeforeConnecting) { this.numberOfSubscriberBeforeConnecting = numberOfSubscriberBeforeConnecting; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + QuarkusEmitterConfiguration that = (QuarkusEmitterConfiguration) o; + return overflowBufferSize == that.overflowBufferSize + && broadcast == that.broadcast + && numberOfSubscriberBeforeConnecting == that.numberOfSubscriberBeforeConnecting + && Objects.equals(name, that.name) + && Objects.equals(emitterType, that.emitterType) + && overflowBufferStrategy == that.overflowBufferStrategy; + } + + @Override + public int hashCode() { + return Objects.hash(name, emitterType, overflowBufferStrategy, overflowBufferSize, broadcast, + numberOfSubscriberBeforeConnecting); + } + + @Override + public String toString() { + return "QuarkusEmitterConfiguration{" + + "name='" + name + '\'' + + ", emitterType=" + emitterType + + ", overflowBufferStrategy=" + overflowBufferStrategy + + ", overflowBufferSize=" + overflowBufferSize + + ", broadcast=" + broadcast + + ", numberOfSubscriberBeforeConnecting=" + numberOfSubscriberBeforeConnecting + + '}'; + } } From cc89874c3b58808826d388377e923adc7b848c23 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Mon, 18 Dec 2023 09:06:22 +0100 Subject: [PATCH 17/36] Bump Smallrye RM from 4.12.0 to 4.13.0 --- bom/application/pom.xml | 2 +- docs/src/main/asciidoc/kafka.adoc | 135 +++++++++++++++++- .../binder/ReactiveMessagingProcessor.java | 33 +++++ extensions/micrometer/runtime/pom.xml | 6 + .../MicrometerObservationCollector.java | 77 ++++++++++ .../runtime/config/MicrometerConfig.java | 2 + .../config/ReactiveMessagingConfigGroup.java | 35 +++++ .../kafka/deployment/DotNames.java | 3 +- ...allRyeReactiveMessagingKafkaProcessor.java | 57 ++++++-- .../deployment/DefaultSerdeConfigTest.java | 40 ++++++ .../reactive-messaging-kafka/pom.xml | 20 +++ .../io/quarkus/it/kafka/KafkaRequest.java | 37 +++++ .../src/main/resources/application.properties | 4 + .../quarkus/it/kafka/KafkaConnectorTest.java | 38 +++++ 14 files changed, 471 insertions(+), 18 deletions(-) create mode 100644 extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/ReactiveMessagingProcessor.java create mode 100644 extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/reactivemessaging/MicrometerObservationCollector.java create mode 100644 extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/ReactiveMessagingConfigGroup.java create mode 100644 integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/KafkaRequest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 5f261e167e978..4aaaea4907093 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -62,7 +62,7 @@ 1.0.13 3.0.1 3.7.2 - 4.12.0 + 4.13.0 2.4.0 2.1.2 2.1.1 diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index 7631552643070..a1eebb82de887 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -492,11 +492,19 @@ image::kafka-one-app-one-consumer.png[alt=Architecture, width=60%, align=center] . *Multiple consumer threads inside a consumer group* + -For a given application instance, the number of consumers inside the consumer group can be configured using `mp.messaging.incoming.$channel.partitions` property. +For a given application instance, the number of consumers inside the consumer group can be configured using `mp.messaging.incoming.$channel.concurrency` property. The partitions of the subscribed topic will be divided among the consumer threads. -Note that if the `partitions` value exceed the number of partitions of the topic, some consumer threads won't be assigned any partitions. +Note that if the `concurrency` value exceed the number of partitions of the topic, some consumer threads won't be assigned any partitions. + image::kafka-one-app-two-consumers.png[alt=Architecture, width=60%, align=center] ++ +[NOTE] +.Deprecation +==== +The https://smallrye.io/smallrye-reactive-messaging/latest/concepts/incoming-concurrency/[concurrency attribute] +provides a connector agnostic way for non-blocking concurrent channels and replaces the Kafka connector specific `partitions` attribute. +The `partitions` attribute is therefore deprecated and will be removed in future releases. +==== . *Multiple consumer applications inside a consumer group* + @@ -658,6 +666,36 @@ mp.messaging.incoming.your-channel.group.id=${quarkus.uuid} IMPORTANT: If the `group.id` attribute is not set, it defaults the `quarkus.application.name` configuration property. +==== Manual topic-partition assignment + +The `assign-seek` channel attribute allows manually assigning topic-partitions to a Kafka incoming channel, +and optionally seek to a specified offset in the partition to start consuming records. +If `assign-seek` is used, the consumer will not be dynamically subscribed to topics, +but instead will statically assign the described partitions. +In manual topic-partition rebalancing doesn't happen and therefore rebalance listeners are never called. + +The attribute takes a list of triplets separated by commas: `::`. + +For example, the configuration + +[source, properties] +---- +mp.messaging.incoming.data.assign-seek=topic1:0:10, topic2:1:20 +---- + +assigns the consumer to: + +- Partition 0 of topic 'topic1', setting the initial position at offset 10. +- Partition 1 of topic 'topic2', setting the initial position at offset 20. + +The topic, partition, and offset in each triplet can have the following variations: + +- If the topic is omitted, the configured topic will be used. +- If the offset is omitted, partitions are assigned to the consumer but won't be sought to offset. +- If offset is 0, it seeks to the beginning of the topic-partition. +- If offset is -1, it seeks to the end of the topic-partition. + + === Receiving Kafka Records in Batches By default, incoming methods receive each Kafka record individually. @@ -1112,7 +1150,7 @@ The new `Emitter.send` method returns a `CompletionStage` completed when the pro ==== [NOTE] -.Depreciation +.Deprecation ==== `MutinyEmitter#send(Message msg)` method is deprecated in favor of following methods receiving `Message` for emitting: @@ -1376,6 +1414,55 @@ If you'd like to consume records only written and committed inside a Kafka trans mp.messaging.incoming.prices-in.isolation.level=read_committed ---- +== Kafka Request-Reply + +The Kafka Request-Reply pattern allows to publish a request record to a Kafka topic and then await for a reply record that responds to the initial request. +The Kafka connector provides the `KafkaRequestReply` custom emitter that implements the requestor (or the client) of the request-reply pattern for Kafka outbound channels: + +It can be injected as a regular emitter `@Channel`: + +[source, java] +---- +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +@Path("/kafka") +public class KafkaRequestReplyEmitter { + + @Channel("request-reply") + KafkaRequestReply requestReply; + + @POST + @Path("/req-rep") + @Produces(MediaType.TEXT_PLAIN) + public Uni post(Integer request) { + return requestReply.request(request); + } + +} +---- + +The request method publishes the record to the configured target topic of the outgoing channel, +and polls a reply topic (by default, the target topic with `-replies` suffix) for a reply record. +When the reply is received the returned `Uni` is completed with the record value. +The request send operation generates a **correlation id** and sets a header (by default `REPLY_CORRELATION_ID`), +which it expects to be sent back in the reply record. + +The replier can be implemented using a Reactive Messaging processor (see <>). + +For more information on Kafka Request Reply feature and advanced configuration options, +see the https://smallrye.io/smallrye-reactive-messaging/latest/kafka/request-reply/[Smallrye Reactive Messaging Documentation]. + +[[processing-messages]] == Processing Messages Applications streaming data often need to consume some events from a topic, process them and publish the result to a different topic. @@ -1944,6 +2031,34 @@ and for an outgoing channel checks that the topic used by the producer exist in Note that to achieve this, an _admin connection_ is required. You can adjust the timeout for topic verification calls to the broker using the `health-topic-verification-timeout` configuration. +== Observability + +If the xref:opentelemetry.adoc[OpenTelemetry extension] is present, +then the Kafka connector channels work out-of-the-box with the OpenTelemetry Tracing. +Messages written to Kafka topics propagate the current tracing span. +On incoming channels, if a consumed Kafka record contains tracing information the message processing inherits the message span as parent. + +Tracing can be disabled explicitly per channel: + +[source, properties] +---- +mp.messaging.incoming.data.tracing-enabled=false +---- + +If the xref:telemetry-micrometer.adoc[Micrometer extension] is present, +then Kafka producer and consumer clients metrics are exposed as Micrometer meters. + +Per channel metrics are also exposed as Micrometer meters. +The number of messages produced or received per channel, acknowledgments and duration of processing are exposed. + +The messaging meters can be disabled: + +[source, properties] +---- +quarkus.micrometer.binder.messaging.enabled=false +---- + + == Kafka Streams This is described in a dedicated guide: xref:kafka-streams.adoc[Using Apache Kafka Streams]. @@ -2204,6 +2319,20 @@ With in-memory channels we were able to test application code processing message Note that different in-memory channels are independent, and switching channel connector to in-memory does not simulate message delivery between channels configured to the same Kafka topic. ==== +==== Context propagation with InMemoryConnector + +By default, in-memory channels dispatch messages on the caller thread, which would be the main thread in unit tests. + +The `quarkus-test-vertx` dependency provides the `@io.quarkus.test.vertx.RunOnVertxContext` annotation, +which when used on a test method, executes the test on a Vert.x context. + +However, most of the other connectors handle context propagation dispatching messages on separate duplicated Vert.x contexts. + +If your tests are dependent on context propagation, +you can configure the in-memory connector channels with the `run-on-vertx-context` attribute to dispatch events, +including messages and acknowledgements, on a Vert.x context. +Alternatively you can switch this behaviour using the `InMemorySource#runOnVertxContext` method. + === Testing using a Kafka broker If you are using <>, a Kafka broker will be started and available throughout the tests, unless it is disabled in `%test` profile. diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/ReactiveMessagingProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/ReactiveMessagingProcessor.java new file mode 100644 index 0000000000000..9736592cbc5aa --- /dev/null +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/ReactiveMessagingProcessor.java @@ -0,0 +1,33 @@ +package io.quarkus.micrometer.deployment.binder; + +import java.util.function.BooleanSupplier; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.micrometer.runtime.MicrometerRecorder; +import io.quarkus.micrometer.runtime.config.MicrometerConfig; + +public class ReactiveMessagingProcessor { + + static final String MESSAGE_OBSERVATION_COLLECTOR = "io.smallrye.reactive.messaging.observation.MessageObservationCollector"; + static final String METRICS_BEAN_CLASS = "io.quarkus.micrometer.runtime.binder.reactivemessaging.MicrometerObservationCollector"; + static final Class MESSAGE_OBSERVATION_COLLECTOR_CLASS = MicrometerRecorder + .getClassForName(MESSAGE_OBSERVATION_COLLECTOR); + + static class ReactiveMessagingSupportEnabled implements BooleanSupplier { + MicrometerConfig mConfig; + + public boolean getAsBoolean() { + return MESSAGE_OBSERVATION_COLLECTOR_CLASS != null && + mConfig.checkBinderEnabledWithDefault(mConfig.binder.messaging); + } + } + + @BuildStep(onlyIf = ReactiveMessagingSupportEnabled.class) + AdditionalBeanBuildItem createCDIEventConsumer() { + return AdditionalBeanBuildItem.builder() + .addBeanClass(METRICS_BEAN_CLASS) + .setUnremovable() + .build(); + } +} diff --git a/extensions/micrometer/runtime/pom.xml b/extensions/micrometer/runtime/pom.xml index ba26155dabbb3..7d803375107bf 100644 --- a/extensions/micrometer/runtime/pom.xml +++ b/extensions/micrometer/runtime/pom.xml @@ -90,6 +90,12 @@ true + + io.smallrye.reactive + smallrye-reactive-messaging-api + true + + org.apache.kafka kafka-clients diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/reactivemessaging/MicrometerObservationCollector.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/reactivemessaging/MicrometerObservationCollector.java new file mode 100644 index 0000000000000..2bb89c40a3211 --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/reactivemessaging/MicrometerObservationCollector.java @@ -0,0 +1,77 @@ +package io.quarkus.micrometer.runtime.binder.reactivemessaging; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class MicrometerObservationCollector + implements MessageObservationCollector { + + @Inject + @ConfigProperty(name = "quarkus.messaging.observation.micrometer.enabled", defaultValue = "true") + boolean enabled; + + @Override + public MicrometerContext initObservation(String channel, boolean incoming, boolean emitter) { + if (enabled) { + return new MicrometerContext(channel); + } + return null; + } + + @Override + public MessageObservation onNewMessage(String channel, Message message, MicrometerContext ctx) { + ctx.msgCount.increment(); + return new DefaultMessageObservation(channel); + } + + public static class MicrometerContext implements ObservationContext { + final Counter msgCount; + final Timer duration; + final Counter acks; + final Counter nacks; + + public MicrometerContext(String channel) { + Tags tags = Tags.of(Tag.of("channel", channel)); + this.msgCount = Counter.builder("quarkus.messaging.message.count") + .description("The number of messages observed") + .tags(tags) + .register(Metrics.globalRegistry); + this.duration = Timer.builder("quarkus.messaging.message.duration") + .description("The duration of the message processing") + .tags(tags) + .register(Metrics.globalRegistry); + this.acks = Counter.builder("quarkus.messaging.message.acks") + .description("The number of messages processed successfully") + .tags(tags) + .register(Metrics.globalRegistry); + this.nacks = Counter.builder("quarkus.messaging.message.failures") + .description("The number of messages processed with failures") + .tags(tags) + .register(Metrics.globalRegistry); + } + + @Override + public void complete(MessageObservation observation) { + if (observation.getReason() == null) { + acks.increment(); + } else { + nacks.increment(); + } + duration.record(observation.getCompletionDuration()); + } + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java index 7286227a22b23..cef3d1f52a5e1 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java @@ -105,6 +105,8 @@ public static class BinderConfig { public GrpcClientConfigGroup grpcClient; + public ReactiveMessagingConfigGroup messaging; + public MPMetricsConfigGroup mpMetrics; /** diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/ReactiveMessagingConfigGroup.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/ReactiveMessagingConfigGroup.java new file mode 100644 index 0000000000000..c4580a6ae9555 --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/ReactiveMessagingConfigGroup.java @@ -0,0 +1,35 @@ +package io.quarkus.micrometer.runtime.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Build / static runtime config for Reactive Messaging Binders + */ +@ConfigGroup +public class ReactiveMessagingConfigGroup implements MicrometerConfig.CapabilityEnabled { + /** + * Kafka metrics support. + *

+ * Support for Reactive Messaging metrics will be enabled if Micrometer support is enabled, + * MessageObservationCollector interface is on the classpath + * and either this value is true, or this value is unset and + * {@code quarkus.micrometer.binder-enabled-default} is true. + */ + @ConfigItem + public Optional enabled; + + @Override + public Optional getEnabled() { + return enabled; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + "{enabled=" + enabled + + '}'; + } +} diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java index 6ae1adb6dd679..3153a56e7e55b 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java @@ -12,7 +12,8 @@ final class DotNames { static final DotName EMITTER = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Emitter.class.getName()); static final DotName MUTINY_EMITTER = DotName.createSimple(io.smallrye.reactive.messaging.MutinyEmitter.class.getName()); - static final DotName KAFKA_EMITTER = DotName.createSimple(io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions.class.getName()); + static final DotName KAFKA_TRANSACTIONS_EMITTER = DotName.createSimple(io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions.class.getName()); + static final DotName KAFKA_REQUEST_REPLY_EMITTER = DotName.createSimple(io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply.class.getName()); static final DotName TARGETED = DotName.createSimple(io.smallrye.reactive.messaging.Targeted.class.getName()); static final DotName TARGETED_MESSAGES = DotName.createSimple(io.smallrye.reactive.messaging.TargetedMessages.class.getName()); diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java index 503b81f253dfb..7a042db169703 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java @@ -271,21 +271,44 @@ void discoverDefaultSerdeConfig(DefaultSerdeDiscoveryState discovery, processKafkaTransactions(discovery, config, channelName, injectionPointType); - Type outgoingType = getOutgoingTypeFromChannelInjectionPoint(injectionPointType); - processOutgoingType(discovery, outgoingType, (keySerializer, valueSerializer) -> { - produceRuntimeConfigurationDefaultBuildItem(discovery, config, - getChannelPropertyKey(channelName, "key.serializer", false), keySerializer); - produceRuntimeConfigurationDefaultBuildItem(discovery, config, - getChannelPropertyKey(channelName, "value.serializer", false), valueSerializer); - - handleAdditionalProperties(channelName, false, discovery, config, keySerializer, valueSerializer); - }, generatedClass, reflection, alreadyGeneratedSerializers); + if (isKafkaRequestReplyEmitter(injectionPointType)) { + Type requestType = injectionPointType.asParameterizedType().arguments().get(0); + Type replyType = injectionPointType.asParameterizedType().arguments().get(1); + processOutgoingType(discovery, requestType, (keySerializer, valueSerializer) -> { + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + getChannelPropertyKey(channelName, "key.serializer", false), keySerializer); + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + getChannelPropertyKey(channelName, "value.serializer", false), valueSerializer); + }, generatedClass, reflection, alreadyGeneratedSerializers); + extractKeyValueType(replyType, (key, value, isBatchType) -> { + Result keyDeserializer = deserializerFor(discovery, key, true, channelName, generatedClass, reflection, + alreadyGeneratedDeserializers, alreadyGeneratedSerializers); + Result valueDeserializer = deserializerFor(discovery, value, false, channelName, generatedClass, reflection, + alreadyGeneratedDeserializers, alreadyGeneratedSerializers); + + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + getChannelPropertyKey(channelName, "reply.key.deserializer", false), keyDeserializer); + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + getChannelPropertyKey(channelName, "reply.value.deserializer", false), valueDeserializer); + handleAdditionalProperties(channelName, false, discovery, config, keyDeserializer, valueDeserializer); + }); + } else { + Type outgoingType = getOutgoingTypeFromChannelInjectionPoint(injectionPointType); + processOutgoingType(discovery, outgoingType, (keySerializer, valueSerializer) -> { + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + getChannelPropertyKey(channelName, "key.serializer", false), keySerializer); + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + getChannelPropertyKey(channelName, "value.serializer", false), valueSerializer); + + handleAdditionalProperties(channelName, false, discovery, config, keySerializer, valueSerializer); + }, generatedClass, reflection, alreadyGeneratedSerializers); + } } } private void processKafkaTransactions(DefaultSerdeDiscoveryState discovery, BuildProducer config, String channelName, Type injectionPointType) { - if (injectionPointType != null && isKafkaEmitter(injectionPointType)) { + if (injectionPointType != null && isKafkaTransactionsEmitter(injectionPointType)) { String transactionalIdKey = getChannelPropertyKey(channelName, "transactional.id", false); String enableIdempotenceKey = getChannelPropertyKey(channelName, "enable.idempotence", false); String acksKey = getChannelPropertyKey(channelName, "acks", false); @@ -496,7 +519,8 @@ private Type getOutgoingTypeFromChannelInjectionPoint(Type injectionPointType) { return null; } - if (isEmitter(injectionPointType) || isMutinyEmitter(injectionPointType) || isKafkaEmitter(injectionPointType)) { + if (isEmitter(injectionPointType) || isMutinyEmitter(injectionPointType) + || isKafkaTransactionsEmitter(injectionPointType)) { return injectionPointType.asParameterizedType().arguments().get(0); } else { return null; @@ -640,13 +664,20 @@ private static boolean isMutinyEmitter(Type type) { && type.asParameterizedType().arguments().size() == 1; } - private static boolean isKafkaEmitter(Type type) { + private static boolean isKafkaTransactionsEmitter(Type type) { // raw type KafkaTransactions is wrong, must be KafkaTransactions - return DotNames.KAFKA_EMITTER.equals(type.name()) + return DotNames.KAFKA_TRANSACTIONS_EMITTER.equals(type.name()) && type.kind() == Type.Kind.PARAMETERIZED_TYPE && type.asParameterizedType().arguments().size() == 1; } + private static boolean isKafkaRequestReplyEmitter(Type type) { + // raw type KafkaRequestReply is wrong, must be KafkaRequestReply + return DotNames.KAFKA_REQUEST_REPLY_EMITTER.equals(type.name()) + && type.kind() == Type.Kind.PARAMETERIZED_TYPE + && type.asParameterizedType().arguments().size() == 2; + } + // --- private static boolean isMessage(Type type) { diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java index e6448af4e145c..7cebd95da8802 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java @@ -59,6 +59,7 @@ import io.smallrye.reactive.messaging.kafka.KafkaRecord; import io.smallrye.reactive.messaging.kafka.KafkaRecordBatch; import io.smallrye.reactive.messaging.kafka.Record; +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -2920,5 +2921,44 @@ TargetedMessages method2(String msg) { } + @Test + void kafkaRequestReply() { + Tuple[] expectations = { + tuple("mp.messaging.outgoing.channel1.value.serializer", "org.apache.kafka.common.serialization.StringSerializer"), + tuple("mp.messaging.outgoing.channel1.reply.value.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer"), + tuple("mp.messaging.outgoing.channel2.key.serializer", "org.apache.kafka.common.serialization.LongSerializer"), + tuple("mp.messaging.outgoing.channel2.value.serializer", "org.apache.kafka.common.serialization.StringSerializer"), + tuple("mp.messaging.outgoing.channel2.reply.value.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer"), + tuple("mp.messaging.outgoing.channel3.key.serializer", "org.apache.kafka.common.serialization.LongSerializer"), + tuple("mp.messaging.outgoing.channel3.value.serializer", "org.apache.kafka.common.serialization.StringSerializer"), + tuple("mp.messaging.outgoing.channel3.reply.key.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer"), + tuple("mp.messaging.outgoing.channel3.reply.value.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer"), + tuple("mp.messaging.outgoing.channel4.value.serializer", "io.apicurio.registry.serde.avro.AvroKafkaSerializer"), + tuple("mp.messaging.outgoing.channel4.reply.key.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer"), + tuple("mp.messaging.outgoing.channel4.reply.value.deserializer", "io.quarkus.kafka.client.serialization.JsonObjectDeserializer"), + }; + doTest(expectations, AvroDto.class, RequestReply.class); + } + + private static class RequestReply { + + @Inject + @Channel("channel1") + KafkaRequestReply requestReply; + + @Inject + @Channel("channel2") + KafkaRequestReply, Integer> requestReply2; + + @Inject + @Channel("channel3") + KafkaRequestReply, Record> requestReply3; + + @Inject + @Channel("channel4") + KafkaRequestReply> requestReply4; + + } + } diff --git a/integration-tests/reactive-messaging-kafka/pom.xml b/integration-tests/reactive-messaging-kafka/pom.xml index fa79fb5107782..5227f16834072 100644 --- a/integration-tests/reactive-messaging-kafka/pom.xml +++ b/integration-tests/reactive-messaging-kafka/pom.xml @@ -47,6 +47,12 @@ quarkus-smallrye-health + + + io.quarkus + quarkus-micrometer-registry-prometheus + + io.quarkus @@ -147,6 +153,20 @@ + + + io.quarkus + quarkus-micrometer-registry-prometheus-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-smallrye-reactive-messaging-kafka-deployment diff --git a/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/KafkaRequest.java b/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/KafkaRequest.java new file mode 100644 index 0000000000000..a00d0de1782dc --- /dev/null +++ b/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/KafkaRequest.java @@ -0,0 +1,37 @@ +package io.quarkus.it.kafka; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.kafka.reply.KafkaRequestReply; + +@ApplicationScoped +@Path("/kafka") +public class KafkaRequest { + + @Channel("request-reply") + KafkaRequestReply requestReply; + + @POST + @Path("/req-rep") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + public String post(Integer request) { + return requestReply.request(request).await().indefinitely(); + } + + @Incoming("request") + @Outgoing("rep") + public String process(int request) { + return "reply-" + request; + } + +} diff --git a/integration-tests/reactive-messaging-kafka/src/main/resources/application.properties b/integration-tests/reactive-messaging-kafka/src/main/resources/application.properties index 89117fa6769b0..a4635331aff3c 100644 --- a/integration-tests/reactive-messaging-kafka/src/main/resources/application.properties +++ b/integration-tests/reactive-messaging-kafka/src/main/resources/application.properties @@ -42,3 +42,7 @@ mp.messaging.incoming.data-with-metadata-in.topic=data-with-metadata mp.messaging.outgoing.data-for-keyed-out.topic=data-for-keyed mp.messaging.incoming.data-for-keyed-in.topic=data-for-keyed +mp.messaging.outgoing.request-reply.topic=request +mp.messaging.outgoing.request-reply.reply.batch=true + +quarkus.micrometer.binder.messaging.enabled=true diff --git a/integration-tests/reactive-messaging-kafka/src/test/java/io/quarkus/it/kafka/KafkaConnectorTest.java b/integration-tests/reactive-messaging-kafka/src/test/java/io/quarkus/it/kafka/KafkaConnectorTest.java index 82bd9d316defb..98dbd1998899d 100644 --- a/integration-tests/reactive-messaging-kafka/src/test/java/io/quarkus/it/kafka/KafkaConnectorTest.java +++ b/integration-tests/reactive-messaging-kafka/src/test/java/io/quarkus/it/kafka/KafkaConnectorTest.java @@ -1,13 +1,19 @@ package io.quarkus.it.kafka; import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.containsString; +import java.util.ArrayList; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import io.quarkus.arc.Arc; import io.quarkus.test.common.QuarkusTestResource; @@ -20,6 +26,7 @@ @QuarkusTest @QuarkusTestResource(KafkaCompanionResource.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class KafkaConnectorTest { protected static final TypeRef> TYPE_REF = new TypeRef>() { @@ -31,6 +38,7 @@ public class KafkaConnectorTest { }; @Test + @Order(1) public void testPeople() { await().untilAsserted(() -> Assertions.assertEquals(get("/kafka/people").as(TYPE_REF).size(), 6)); await().untilAsserted(() -> { @@ -44,23 +52,27 @@ public void testPeople() { } @Test + @Order(2) public void testPets() { await().untilAsserted(() -> Assertions.assertEquals(get("/kafka/pets").as(TYPE_REF).size(), 3)); } @Test + @Order(3) public void testFruits() { await().untilAsserted(() -> Assertions.assertEquals(get("/kafka/fruits").as(FRUIT_TYPE_REF).size(), 5)); } @Test @DisabledOnIntegrationTest + @Order(4) public void testPrices() { KafkaRepeatableReceivers repeatableReceivers = Arc.container().instance(KafkaRepeatableReceivers.class).get(); await().untilAsserted(() -> Assertions.assertEquals(repeatableReceivers.getPrices().size(), 6)); } @Test + @Order(5) public void testDataWithMetadata() { await().untilAsserted(() -> { Map map = get("/kafka/data-with-metadata").as(new TypeRef>() { @@ -73,6 +85,7 @@ public void testDataWithMetadata() { } @Test + @Order(6) public void testDataForKeyed() { await().untilAsserted(() -> { List list = get("/kafka/data-for-keyed").as(new TypeRef>() { @@ -84,4 +97,29 @@ public void testDataForKeyed() { }); } + @Test + @Order(7) + public void testRequestReply() { + List replies = new ArrayList<>(); + replies.add(given().contentType("application/json").body("1").post("/kafka/req-rep").asString()); + replies.add(given().contentType("application/json").body("2").post("/kafka/req-rep").asString()); + replies.add(given().contentType("application/json").body("3").post("/kafka/req-rep").asString()); + replies.add(given().contentType("application/json").body("4").post("/kafka/req-rep").asString()); + replies.add(given().contentType("application/json").body("5").post("/kafka/req-rep").asString()); + Assertions.assertIterableEquals(List.of("reply-1", "reply-2", "reply-3", "reply-4", "reply-5"), replies); + } + + @Test + @Order(8) + void testPrometheusScrapeEndpointOpenMetrics() { + given().header("Accept", "text/plain; version=0.0.4; charset=utf-8") + .when().get("/q/metrics") + .then().statusCode(200) + .body(containsString("quarkus_messaging_message_duration_seconds_max")) + .body(containsString("quarkus_messaging_message_duration_seconds_sum")) + .body(containsString("quarkus_messaging_message_duration_seconds_count")) + .body(containsString("quarkus_messaging_message_count_total")) + .body(containsString("quarkus_messaging_message_acks_total")); + } + } From 2240a7596f0c5f6ee7dba3558617d2b7a02cb2b0 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 2 Jan 2024 13:13:06 +0000 Subject: [PATCH 18/36] Clarify how Keycloak Dev Service can be used to test Keycloak authorization --- .../security-keycloak-authorization.adoc | 21 ++++++++++++++++++- ...rity-oidc-bearer-token-authentication.adoc | 6 ++++++ .../security-openid-connect-dev-services.adoc | 10 +++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/security-keycloak-authorization.adoc b/docs/src/main/asciidoc/security-keycloak-authorization.adoc index 7470853aee39d..aa2c96b858f8b 100644 --- a/docs/src/main/asciidoc/security-keycloak-authorization.adoc +++ b/docs/src/main/asciidoc/security-keycloak-authorization.adoc @@ -233,7 +233,7 @@ To run the application in dev mode, use: include::{includes}/devtools/dev.adoc[] -xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] will launch a Keycloak container and import a `quarkus-realm.json`. +xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] will launch a Keycloak container and import the link:{quickstarts-tree-url}/security-keycloak-authorization-quickstart/config/quarkus-realm.json[realm configuration file]. Open a xref:dev-ui.adoc[Dev UI] available at http://localhost:8080/q/dev-ui[/q/dev-ui] and click on a `Provider: Keycloak` link in an `OpenID Connect` `Dev UI` card. @@ -246,6 +246,24 @@ You will be asked to log in into a `Single Page Application` provided by `OpenID ** accessing `/api/admin` will return `200` ** accessing `/api/users/me` will return `200` +If you have started xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] without importing a realm file such as link:{quickstarts-tree-url}/security-keycloak-authorization-quickstart/config/quarkus-realm.json[quarkus-realm.json] which is already configured to support Keycloak Authorization then a default `quarkus` realm without Keycloak authorization policies will be created. In this case you must select the `Keycloak Admin` link in the `OpenId Connect` Dev UI card and configure link:https://www.keycloak.org/docs/latest/authorization_services/index.html[Keycloak Authorization] in the default `quarkus` realm. + +The `Keycloak Admin` link is easy to find in Dev UI: + +image::dev-ui-oidc-keycloak-card.png[alt=Dev UI OpenID Connect Card,role="center"] + +When logging in the Keycloak admin console, the username is `admin` and the password is `admin`. + +If your application configures Keycloak authorization with link:https://www.keycloak.org/docs/latest/authorization_services/index.html#_policy_js[JavaScript policies] that are deployed to Keycloak in a jar file then you can configure `Dev Services for Keycloak` to copy this jar to the Keycloak container, for example: + +[source,properties] +---- +quarkus.keycloak.devservices.resource-aliases.policies=/policies.jar <1> +quarkus.keycloak.devservices.resource-mappings.policies=/opt/keycloak/providers/policies.jar <2> +---- +<1> `policies` alias is created for the `/policies.jar` classpath resource. Policy jars can also be located in the file system. +<2> The policies jar is mapped to the `/opt/keycloak/providers/policies.jar` container location. + == Running the Application in JVM mode When you're done playing with the `dev` mode" you can run it as a standard Java application. @@ -278,6 +296,7 @@ After getting a cup of coffee, you'll be able to run this binary directly: ./target/security-keycloak-authorization-quickstart-runner ---- +[[testing]] == Testing the Application See <> section above about testing your application in a dev mode. diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index d4038f675b542..a51aeb07c06b6 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -410,6 +410,12 @@ Please see xref:security-openid-connect-client-reference.adoc#token-propagation[ [[integration-testing]] === Testing +[NOTE] +==== +If you have to test Quarkus OIDC service endpoints that require xref:security-keycloak-authorization.adoc[Keycloak Authorization] then you must follow the xref:security-keycloak-authorization.adoc#testing[Test Keycloak Authorization] section. +==== + + Start by adding the following dependencies to your test project: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index cb9888041a398..3e6e14d137609 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -272,6 +272,16 @@ image::dev-ui-keycloak-admin.png[alt=Dev UI OpenID Connect Keycloak Page - Keycl Sign in to Keycloak as `admin:admin` in order to further customize the realm properties, create or import a new realm, export the realm. +You can also copy classpath and file system resources to the container. For example, if your application configures Keycloak authorization with link:https://www.keycloak.org/docs/latest/authorization_services/index.html#_policy_js[JavaScript policies] that are deployed to Keycloak in a jar file then you can configure `Dev Services for Keycloak` to copy this jar to the Keycloak container as follows: + +[source,properties] +---- +quarkus.keycloak.devservices.resource-aliases.policies=/policies.jar <1> +quarkus.keycloak.devservices.resource-mappings.policies=/opt/keycloak/providers/policies.jar <2> +---- +<1> `policies` alias is created for the classpath `/policies.jar` resource. Policy jars can also be located in the file system. +<2> The policies jar is mapped to the `/opt/keycloak/providers/policies.jar` container location. + == Disable Dev Services for Keycloak `Dev Services For Keycloak` will not be activated if either `quarkus.oidc.auth-server-url` is already initialized or the default OIDC tenant is disabled with `quarkus.oidc.tenant.enabled=false`, irrespectively of whether you work with Keycloak or not. From 6f4d300c4a91626a536bade5ee0037527f7a2de8 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 2 Jan 2024 13:23:47 +0000 Subject: [PATCH 19/36] Bump Keycloak version to 23.0.3 --- bom/application/pom.xml | 2 +- build-parent/pom.xml | 2 +- .../src/main/asciidoc/security-openid-connect-dev-services.adoc | 2 +- .../oidc/deployment/devservices/keycloak/DevServicesConfig.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 5f261e167e978..46f1bad6fd8ab 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -191,7 +191,7 @@ 5.8.0 4.13.0 2.0.2.Final - 23.0.1 + 23.0.3 1.15.1 3.42.0 2.23.0 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 11a3b463f3a72..3d781b7c659c5 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -104,7 +104,7 @@ - 23.0.1 + 23.0.3 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index cb9888041a398..129f3aed37883 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -225,7 +225,7 @@ For more information, see xref:security-oidc-bearer-token-authentication.adoc#in [[keycloak-initialization]] === Keycloak Initialization -The `quay.io/keycloak/keycloak:23.0.1` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. +The `quay.io/keycloak/keycloak:23.0.3` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. `quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name. For example, set it to `quay.io/keycloak/keycloak:19.0.3-legacy` to use a Keycloak distribution powered by WildFly. Note that only a Quarkus based Keycloak distribution is available starting from Keycloak `20.0.0`. diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 913ca527624dc..a12af4f1bd7bb 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -35,7 +35,7 @@ public class DevServicesConfig { * string. * Set 'quarkus.keycloak.devservices.keycloak-x-image' to override this check. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:23.0.1") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:23.0.3") public String imageName; /** From 4814ce004c5a973c86a1e08e8e798cd9229ae5dc Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 2 Jan 2024 15:48:56 -0500 Subject: [PATCH 20/36] Bump smallrye-open-api from 3.7.0 to 3.8.0 Signed-off-by: Michael Edgar --- 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 6c00c74b469bd..8eb09e6e00fc7 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -54,7 +54,7 @@ 3.4.4 4.0.4 4.0.0 - 3.7.0 + 3.8.0 2.6.1 6.2.6 4.4.0 From 1aa4604aeeb6ad4f9a2c8de9067c7c742531bb35 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 2 Jan 2024 22:26:01 +0000 Subject: [PATCH 21/36] Do not expand config properties for Gradle Workers --- .../io/quarkus/gradle/tasks/QuarkusBuildTask.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index 13c97cfd722a1..88196dbba173a 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -28,6 +28,7 @@ import io.quarkus.gradle.QuarkusPlugin; import io.quarkus.gradle.tasks.worker.BuildWorker; import io.quarkus.maven.dependency.GACTV; +import io.smallrye.config.Expressions; /** * Base class for the {@link QuarkusBuildDependencies}, {@link QuarkusBuildCacheableAppParts}, {@link QuarkusBuild} tasks @@ -207,12 +208,14 @@ void generateBuild() { ApplicationModel appModel = resolveAppModelForBuild(); Map configMap = new HashMap<>(); - for (Map.Entry entry : extension().buildEffectiveConfiguration(appModel.getAppArtifact()).configMap() - .entrySet()) { - if (entry.getKey().startsWith("quarkus.")) { - configMap.put(entry.getKey(), entry.getValue()); + EffectiveConfig effectiveConfig = extension().buildEffectiveConfiguration(appModel.getAppArtifact()); + Expressions.withoutExpansion(() -> { + for (Map.Entry entry : effectiveConfig.configMap().entrySet()) { + if (entry.getKey().startsWith("quarkus.")) { + configMap.put(entry.getKey(), effectiveConfig.config().getRawValue(entry.getKey())); + } } - } + }); getLogger().info("Starting Quarkus application build for package type {}", packageType); From d5f1e25a275faeed39754baf5f1b083b70ff3f66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:37:19 +0000 Subject: [PATCH 22/36] Bump manusa/actions-setup-minikube from 2.9.0 to 2.10.0 Bumps [manusa/actions-setup-minikube](https://github.com/manusa/actions-setup-minikube) from 2.9.0 to 2.10.0. - [Release notes](https://github.com/manusa/actions-setup-minikube/releases) - [Commits](https://github.com/manusa/actions-setup-minikube/compare/v2.9.0...v2.10.0) --- updated-dependencies: - dependency-name: manusa/actions-setup-minikube dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci-istio.yml | 2 +- .github/workflows/ci-kubernetes.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-istio.yml b/.github/workflows/ci-istio.yml index 8fc760b6d499e..91989a411e188 100644 --- a/.github/workflows/ci-istio.yml +++ b/.github/workflows/ci-istio.yml @@ -57,7 +57,7 @@ jobs: shell: bash run: tar -xzf maven-repo.tgz -C ~ - name: Set up Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.9.0 + uses: manusa/actions-setup-minikube@v2.10.0 with: minikube version: v1.16.0 kubernetes version: ${{ matrix.kubernetes }} diff --git a/.github/workflows/ci-kubernetes.yml b/.github/workflows/ci-kubernetes.yml index 68caaeb5069cd..f534095a5fa22 100644 --- a/.github/workflows/ci-kubernetes.yml +++ b/.github/workflows/ci-kubernetes.yml @@ -57,7 +57,7 @@ jobs: shell: bash run: tar -xzf maven-repo.tgz -C ~ - name: Set up Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.9.0 + uses: manusa/actions-setup-minikube@v2.10.0 with: minikube version: v1.16.0 kubernetes version: ${{ matrix.kubernetes }} From a110cea7bb88d0b934f7717d9cfa8fe7b95f004c Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 3 Jan 2024 10:20:23 +0200 Subject: [PATCH 23/36] Register JsonSubTypes.Type values for native mode Fixes: #37942 --- .../jackson/deployment/JacksonProcessor.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java index 0560f937fce55..cdfa668c0ca34 100644 --- a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java +++ b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java @@ -25,6 +25,7 @@ import org.jboss.jandex.Type; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.SimpleObjectIdResolver; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; @@ -79,7 +80,7 @@ public class JacksonProcessor { private static final DotName JSON_AUTO_DETECT = DotName.createSimple(JsonAutoDetect.class.getName()); private static final DotName JSON_TYPE_ID_RESOLVER = DotName.createSimple(JsonTypeIdResolver.class.getName()); - + private static final DotName JSON_SUBTYPES = DotName.createSimple(JsonSubTypes.class.getName()); private static final DotName JSON_CREATOR = DotName.createSimple("com.fasterxml.jackson.annotation.JsonCreator"); private static final DotName JSON_NAMING = DotName.createSimple("com.fasterxml.jackson.databind.annotation.JsonNaming"); @@ -98,6 +99,7 @@ public class JacksonProcessor { // this list can probably be enriched with more modules private static final List MODULES_NAMES_TO_AUTO_REGISTER = Arrays.asList(TIME_MODULE, JDK8_MODULE, PARAMETER_NAMES_MODULE); + private static final String[] EMPTY_STRING = new String[0]; @Inject CombinedIndexBuildItem combinedIndexBuildItem; @@ -271,6 +273,25 @@ void register( } } + // register @JsonSubTypes.Type values for reflection + Set subTypeTypesNames = new HashSet<>(); + for (AnnotationInstance subTypeInstance : index.getAnnotations(JSON_SUBTYPES)) { + AnnotationValue subTypeValue = subTypeInstance.value(); + if (subTypeValue != null) { + for (AnnotationInstance subTypeTypeInstance : subTypeValue.asNestedArray()) { + AnnotationValue subTypeTypeValue = subTypeTypeInstance.value(); + if (subTypeTypeValue != null) { + subTypeTypesNames.add(subTypeTypeValue.asClass().name().toString()); + } + } + + } + } + if (!subTypeTypesNames.isEmpty()) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder(subTypeTypesNames.toArray(EMPTY_STRING)) + .methods().fields().build()); + } + // this needs to be registered manually since the runtime module is not indexed by Jandex additionalBeans.produce(new AdditionalBeanBuildItem(ObjectMapperProducer.class)); } From 05847006596bd61a976735a8f13ff4990267a119 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 3 Jan 2024 12:17:07 +0200 Subject: [PATCH 24/36] Fix javadoc of RESTEasy Reactive filters --- .../jboss/resteasy/reactive/server/ServerRequestFilter.java | 6 +++--- .../resteasy/reactive/server/ServerResponseFilter.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java index 168378e52f6ba..2e4aaee59dc36 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java @@ -90,7 +90,7 @@ /** * Whether the filter is a pre-matching filter - * + *

* Note that this setting and {@link ServerRequestFilter#readBody()} cannot be both set to true. */ boolean preMatching() default false; @@ -108,10 +108,10 @@ /** * If set to {@code true}, the filter will be run after the body has been fully read but before any deserialization takes * place. - * + *

* Note that this change only affects Resource Methods that do result in reading the message body. For all other * Resource Methods that the filter applies to, it will be executed in normal fashion. - * + *

* Also note that this setting and {@link ServerRequestFilter#preMatching()} cannot be both set to true. * * @deprecated use {@link WithFormRead} on your filter to force reading the form values before your filter is invoked. diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerResponseFilter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerResponseFilter.java index a12043ec3a75f..6c162d17c5c7f 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerResponseFilter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerResponseFilter.java @@ -14,7 +14,7 @@ /** * When used on a method, then an implementation of {@link jakarta.ws.rs.container.ContainerResponseContext} is generated * that calls the annotated method with the proper arguments - * + *

* The idea behind using this is to make it much to write a {@code ServerResponseFilter} as all the necessary information * is passed as arguments to the method instead of forcing the author to use a mix of {@code @Context} and programmatic CDI * look-ups. From b364fe9e0dad4c74b3a735d93cb47d3ff469b6e6 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 3 Jan 2024 12:22:04 +0200 Subject: [PATCH 25/36] Don't fail if config is not a directory Fixes: #37903 --- .../io/quarkus/runtime/configuration/ConfigDiagnostic.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java index 574ae74cf169e..d073ca201d7f9 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java @@ -6,6 +6,7 @@ import java.net.URI; import java.nio.file.DirectoryStream; import java.nio.file.Files; +import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; @@ -186,6 +187,9 @@ public static Set configFiles(Path configFilesLocation) throws IOExcepti for (Path candidate : candidates) { configFiles.add(candidate.toUri().toURL().toString()); } + } catch (NotDirectoryException ignored) { + log.debugf("File %s is not a directory", configFilesLocation.toAbsolutePath()); + return Collections.emptySet(); } return configFiles; } From 6053da3880cdbb5a53092b2c320be89cad77f7a8 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Sat, 16 Dec 2023 20:49:27 +0000 Subject: [PATCH 26/36] Update SmallRye Config to 3.5.1 --- bom/application/pom.xml | 2 +- .../BuildTimeConfigurationReader.java | 7 + .../RunTimeConfigurationGenerator.java | 142 +----------------- .../steps/ConfigDescriptionBuildStep.java | 3 +- .../steps/ConfigGenerationBuildStep.java | 91 ++++++++++- .../configuration/RuntimeConfigBuilder.java | 1 + .../StaticInitConfigBuilder.java | 1 + .../quarkus/gradle/tasks/EffectiveConfig.java | 110 ++++++-------- .../tasks/QuarkusShowEffectiveConfig.java | 8 +- .../gradle/tasks/EffectiveConfigTest.java | 96 ++++-------- .../application.properties | 2 - .../application.yaml | 5 - .../application.yml | 5 - .../app-props-and-yaml/application.properties | 1 - .../app-props-and-yaml/application.yaml | 3 - .../app-yaml-and-yml/application.yaml | 3 - .../app-yaml-and-yml/application.yml | 3 - .../overload/1/application.properties | 2 +- .../overload/2/application.yaml | 2 +- .../overload/4/application.properties | 2 +- .../overload/5/application-prod.yaml | 4 + .../overload/5/application.yaml | 3 + .../single-app-props/application.properties | 1 - .../single-app-yaml/application.yaml | 3 - .../single-app-yml/application.yml | 3 - .../ConfigPropertyMapInjectionTest.java | 10 +- 26 files changed, 197 insertions(+), 316 deletions(-) delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.properties delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.yaml delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.yml delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml/application.properties delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml/application.yaml delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-yaml-and-yml/application.yaml delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-yaml-and-yml/application.yml create mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/5/application-prod.yaml create mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/5/application.yaml delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-props/application.properties delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-yaml/application.yaml delete mode 100644 devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-yml/application.yml diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 6c00c74b469bd..de6352319727a 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -51,7 +51,7 @@ 2.0 3.1.1 2.2.0 - 3.4.4 + 3.5.1 4.0.4 4.0.0 3.7.0 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index a701d35b55683..dff8fcb219e91 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -508,10 +508,17 @@ ReadResult run() { nameBuilder.setLength(0); } + // Register defaults for Roots allBuildTimeValues.putAll(getDefaults(buildTimePatternMap)); buildTimeRunTimeValues.putAll(getDefaults(buildTimeRunTimePatternMap)); runTimeDefaultValues.putAll(getDefaults(runTimePatternMap)); + // Register defaults for Mappings + // Runtime defaults are added in ConfigGenerationBuildStep.generateBuilders to include user mappings + for (ConfigClassWithPrefix buildTimeRunTimeMapping : buildTimeRunTimeMappings) { + buildTimeRunTimeValues.putAll(ConfigMappings.getDefaults(buildTimeRunTimeMapping)); + } + SmallRyeConfig runtimeDefaultsConfig = getConfigForRuntimeDefaults(); Set registeredRoots = allRoots.stream().map(RootDefinition::getName).collect(toSet()); registeredRoots.add("quarkus"); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java index b1e95250a5ddf..791010b5438c3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -683,111 +683,6 @@ private MethodDescriptor generateInitGroup(ClassDefinition definition) { return methodDescriptor; } - private static MethodDescriptor generateDefaultValueParse(final ClassCreator dvcc, - final ConfigPatternMap keyMap, final StringBuilder methodName) { - - final Container matched = keyMap.getMatched(); - final boolean hasDefault; - if (matched != null) { - final ClassDefinition.ClassMember member = matched.getClassMember(); - // matched members *must* be item members - assert member instanceof ClassDefinition.ItemMember; - ClassDefinition.ItemMember itemMember = (ClassDefinition.ItemMember) member; - hasDefault = itemMember.getDefaultValue() != null; - } else { - hasDefault = false; - } - - final Iterable names = keyMap.childNames(); - final Map children = new HashMap<>(); - MethodDescriptor wildCard = null; - for (String name : names) { - final int length = methodName.length(); - if (name.equals(ConfigPatternMap.WILD_CARD)) { - methodName.append(":*"); - wildCard = generateDefaultValueParse(dvcc, keyMap.getChild(ConfigPatternMap.WILD_CARD), methodName); - } else { - methodName.append(':').append(name); - final MethodDescriptor value = generateDefaultValueParse(dvcc, keyMap.getChild(name), methodName); - if (value != null) { - children.put(name, value); - } - } - methodName.setLength(length); - } - if (children.isEmpty() && wildCard == null && !hasDefault) { - // skip parse trees with no default values in them - return null; - } - - try (MethodCreator body = dvcc.getMethodCreator(methodName.toString(), String.class, NameIterator.class)) { - body.setModifiers(Opcodes.ACC_PRIVATE); - - final ResultHandle keyIter = body.getMethodParam(0); - // if we've matched the whole thing... - // if (! keyIter.hasNext()) { - try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, keyIter)) - .falseBranch()) { - if (matched != null) { - final ClassDefinition.ClassMember member = matched.getClassMember(); - // matched members *must* be item members - assert member instanceof ClassDefinition.ItemMember; - ClassDefinition.ItemMember itemMember = (ClassDefinition.ItemMember) member; - // match? - final String defaultValue = itemMember.getDefaultValue(); - if (defaultValue != null) { - // matched with default value - // return "defaultValue"; - matchedBody.returnValue(matchedBody.load(defaultValue)); - } else { - // matched but no default value - // return null; - matchedBody.returnValue(matchedBody.loadNull()); - } - } else { - // no match - // return null; - matchedBody.returnValue(matchedBody.loadNull()); - } - } - // } - // branches for each next-string - for (String name : children.keySet()) { - // TODO: string switch - // if (keyIter.nextSegmentEquals(name)) { - try (BytecodeCreator nameMatched = body - .ifNonZero(body.invokeVirtualMethod(NI_NEXT_EQUALS, keyIter, body.load(name))).trueBranch()) { - // keyIter.next(); - nameMatched.invokeVirtualMethod(NI_NEXT, keyIter); - // (generated recursive) - // result = getDefault$..$name(keyIter); - ResultHandle result = nameMatched.invokeVirtualMethod(children.get(name), body.getThis(), keyIter); - // return result; - nameMatched.returnValue(result); - } - // } - } - if (wildCard != null) { - // consume and parse - try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, keyIter)) - .trueBranch()) { - // keyIter.next(); - matchedBody.invokeVirtualMethod(NI_NEXT, keyIter); - // (generated recursive) - // result = getDefault$..$*(keyIter); - final ResultHandle result = matchedBody.invokeVirtualMethod(wildCard, body.getThis(), keyIter); - // return result; - matchedBody.returnValue(result); - } - } - // unknown - // return null; - body.returnValue(body.loadNull()); - - return body.getMethodDescriptor(); - } - } - private void generateEmptyParsers() { MethodCreator body = cc.getMethodCreator(RT_EMPTY_PARSER); body.setModifiers(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC); @@ -1295,37 +1190,6 @@ private FieldDescriptor getOrCreateConverterInstance(Field field, ConverterType return fd; } - private void reportUnknown(final MethodCreator mc) { - mc.setModifiers(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC); - - ResultHandle unknownProperty = mc.getMethodParam(0); - ResultHandle unknown = mc.getMethodParam(1); - - // Ignore all build property names. This is to ignore any properties mapped with @ConfigMapping, because - // these do not fall into the ignored ConfigPattern. - for (String buildTimeProperty : allBuildTimeValues.keySet()) { - ResultHandle equalsResult = mc.invokeVirtualMethod( - MethodDescriptor.ofMethod(Object.class, "equals", boolean.class, Object.class), unknownProperty, - mc.load(buildTimeProperty)); - mc.ifTrue(equalsResult).trueBranch().returnValue(null); - } - - // Ignore recorded runtime property names. This is to ignore any properties mapped with @ConfigMapping, because - // these do not fall into the ignored ConfigPattern. - for (String buildTimeProperty : runTimeDefaultValues.keySet()) { - ResultHandle equalsResult = mc.invokeVirtualMethod( - MethodDescriptor.ofMethod(Object.class, "equals", boolean.class, Object.class), unknownProperty, - mc.load(buildTimeProperty)); - mc.ifTrue(equalsResult).trueBranch().returnValue(null); - } - - // Add the property as unknown only if all checks fail - mc.invokeVirtualMethod(HS_ADD, unknown, unknownProperty); - - mc.returnValue(null); - mc.close(); - } - static final MethodDescriptor KM_NEW = MethodDescriptor.ofConstructor(KeyMap.class); static final MethodDescriptor KM_FIND_OR_ADD = MethodDescriptor.ofMethod(KeyMap.class, "findOrAdd", KeyMap.class, String.class); @@ -1335,13 +1199,13 @@ private void reportUnknown(final MethodCreator mc) { private void generateUnknownFilter() { Set mappedProperties = new HashSet<>(); for (ConfigClassWithPrefix buildTimeMapping : buildTimeConfigResult.getBuildTimeMappings()) { - mappedProperties.addAll(ConfigMappings.getProperties(buildTimeMapping).keySet()); + mappedProperties.addAll(ConfigMappings.getKeys(buildTimeMapping)); } for (ConfigClassWithPrefix staticConfigMapping : buildTimeConfigResult.getBuildTimeRunTimeMappings()) { - mappedProperties.addAll(ConfigMappings.getProperties(staticConfigMapping).keySet()); + mappedProperties.addAll(ConfigMappings.getKeys(staticConfigMapping)); } for (ConfigClassWithPrefix runtimeConfigMapping : buildTimeConfigResult.getRunTimeMappings()) { - mappedProperties.addAll(ConfigMappings.getProperties(runtimeConfigMapping).keySet()); + mappedProperties.addAll(ConfigMappings.getKeys(runtimeConfigMapping)); } // Add a method that generates a KeyMap that can check if a property is mapped by a @ConfigMapping diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java index 3e631cca49a24..93d994f58c834 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java @@ -123,7 +123,8 @@ public void accept(Container node) { private void processMappings(List mappings, List descriptionBuildItems, Properties javaDocProperties, ConfigPhase configPhase) { for (ConfigClassWithPrefix mapping : mappings) { - Map properties = ConfigMappings.getProperties(mapping); + Map properties = ConfigMappings.getProperties(mapping).get(mapping.getKlass()) + .get(mapping.getPrefix()); for (Map.Entry entry : properties.entrySet()) { String propertyName = entry.getKey(); Property property = entry.getValue(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 97c36ea3a1702..b088de15d4225 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -86,6 +86,7 @@ import io.quarkus.runtime.configuration.RuntimeOverrideConfigSource; import io.quarkus.runtime.configuration.RuntimeOverrideConfigSourceBuilder; import io.quarkus.runtime.configuration.StaticInitConfigBuilder; +import io.smallrye.config.ConfigMappings; import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; import io.smallrye.config.ConfigSourceFactory; import io.smallrye.config.ConfigSourceInterceptor; @@ -200,8 +201,17 @@ void generateBuilders( BuildProducer generatedClass, BuildProducer reflectiveClass) throws Exception { + Map defaultValues = new HashMap<>(); + // Default values from extensions @ConfigMapping + for (ConfigClassWithPrefix runTimeMapping : configItem.getReadResult().getRunTimeMappings()) { + defaultValues.putAll(ConfigMappings.getDefaults(runTimeMapping)); + } + // Default values from user @ConfigMapping + for (ConfigMappingBuildItem configMapping : configMappings) { + defaultValues.putAll(ConfigMappings.getDefaults(configMapping.toConfigClassWithPrefix())); + } // Default values from @ConfigRoot - Map defaultValues = new HashMap<>(configItem.getReadResult().getRunTimeDefaultValues()); + defaultValues.putAll(configItem.getReadResult().getRunTimeDefaultValues()); // Default values from build item RunTimeConfigurationDefaultBuildItem override for (RunTimeConfigurationDefaultBuildItem e : runTimeDefaults) { defaultValues.put(e.getKey(), e.getValue()); @@ -225,6 +235,8 @@ void generateBuilders( staticMappings.addAll(configItem.getReadResult().getBuildTimeRunTimeMappings()); Set staticCustomizers = new HashSet<>(staticSafeServices(configCustomizers)); staticCustomizers.add(StaticInitConfigBuilder.class.getName()); + String staticMappingsInfoClassName = "io.quarkus.runtime.generated.StaticInitMappingsInfo"; + generateMappingsInfo(generatedClass, reflectiveClass, staticMappingsInfoClassName, staticMappings); generateConfigBuilder(generatedClass, reflectiveClass, CONFIG_STATIC_NAME, defaultValues, @@ -236,6 +248,7 @@ void generateBuilders( staticSafeServices(configSourceFactories), secretKeyHandlers, staticSafeServices(secretKeyHandlerFactories), + staticMappingsInfoClassName, staticMappings, staticCustomizers, staticInitConfigBuilders.stream().map(StaticInitConfigBuilderBuildItem::getBuilderClassName).collect(toSet())); @@ -248,6 +261,8 @@ void generateBuilders( runTimeMappings.addAll(configItem.getReadResult().getRunTimeMappings()); Set runtimeCustomizers = new HashSet<>(configCustomizers); runtimeCustomizers.add(RuntimeConfigBuilder.class.getName()); + String runtimeMappingsInfoClassName = "io.quarkus.runtime.generated.RunTimeMappingsInfo"; + generateMappingsInfo(generatedClass, reflectiveClass, runtimeMappingsInfoClassName, runTimeMappings); generateConfigBuilder(generatedClass, reflectiveClass, CONFIG_RUNTIME_NAME, defaultValues, @@ -259,6 +274,7 @@ void generateBuilders( configSourceFactories, secretKeyHandlers, secretKeyHandlerFactories, + runtimeMappingsInfoClassName, runTimeMappings, runtimeCustomizers, runTimeConfigBuilders.stream().map(RunTimeConfigBuilderBuildItem::getBuilderClassName).collect(toSet())); @@ -493,6 +509,66 @@ private static String getPathWithoutExtension(Path path) { return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); } + private static final MethodDescriptor MAP_PUT = MethodDescriptor.ofMethod(HashMap.class, + "put", Object.class, Object.class, Object.class); + private static final MethodDescriptor SET_ADD = MethodDescriptor.ofMethod(HashSet.class, + "add", boolean.class, Object.class); + + private static void generateMappingsInfo( + BuildProducer generatedClass, + BuildProducer reflectiveClass, + String className, + Set mappings) { + + try (ClassCreator classCreator = ClassCreator.builder() + .classOutput(new GeneratedClassGizmoAdaptor(generatedClass, true)) + .className(className) + .setFinal(true) + .build()) { + + for (ConfigMappings.ConfigClassWithPrefix mapping : mappings) { + generateMappingNames(classCreator, mapping); + } + + MethodCreator keys = classCreator.getMethodCreator("keys", Set.class); + keys.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC); + ResultHandle set = keys.checkCast(keys.newInstance(MethodDescriptor.ofConstructor(HashSet.class)), Set.class); + for (ConfigMappings.ConfigClassWithPrefix mapping : mappings) { + for (String key : ConfigMappings.getKeys(mapping)) { + keys.invokeVirtualMethod(SET_ADD, set, keys.load(key)); + } + } + keys.returnValue(set); + } + + reflectiveClass.produce(ReflectiveClassBuildItem.builder(className).build()); + } + + private static void generateMappingNames(ClassCreator classCreator, ConfigMappings.ConfigClassWithPrefix mapping) { + MethodCreator method = classCreator.getMethodCreator( + mapping.getKlass().getName().replaceAll("\\.", "_") + "_" + mapping.getPrefix().hashCode(), Map.class); + method.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC); + Map>> properties = ConfigMappings.getNames(mapping); + ResultHandle map = method.checkCast( + method.newInstance(MethodDescriptor.ofConstructor(HashMap.class, int.class), method.load(properties.size())), + Map.class); + for (Map.Entry>> entry : properties.entrySet()) { + ResultHandle groups = method.checkCast(method.newInstance(MethodDescriptor.ofConstructor(HashMap.class, int.class), + method.load(entry.getValue().size())), Map.class); + for (Map.Entry> group : entry.getValue().entrySet()) { + ResultHandle names = method + .checkCast(method.newInstance(MethodDescriptor.ofConstructor(HashSet.class, int.class), + method.load(group.getValue().size())), Set.class); + for (String name : group.getValue()) { + method.invokeVirtualMethod(SET_ADD, names, method.load(name)); + } + method.invokeVirtualMethod(MAP_PUT, groups, method.load(group.getKey()), names); + } + method.invokeVirtualMethod(MAP_PUT, map, method.load(entry.getKey()), groups); + } + method.returnValue(map); + } + private static final MethodDescriptor BUILDER_CUSTOMIZER = MethodDescriptor.ofMethod(SmallRyeConfigBuilderCustomizer.class, "configBuilder", void.class, SmallRyeConfigBuilder.class); @@ -530,6 +606,12 @@ private static String getPathWithoutExtension(Path path) { private static final MethodDescriptor WITH_BUILDER = MethodDescriptor.ofMethod(AbstractConfigBuilder.class, "withBuilder", void.class, SmallRyeConfigBuilder.class, ConfigBuilder.class); + private static final MethodDescriptor WITH_NAMES = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, + "withMappingNames", + SmallRyeConfigBuilder.class, Map.class); + private static final MethodDescriptor WITH_KEYS = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, + "withMappingKeys", + SmallRyeConfigBuilder.class, Set.class); private static void generateConfigBuilder( BuildProducer generatedClass, @@ -544,6 +626,7 @@ private static void generateConfigBuilder( Set configSourceFactories, Set secretKeyHandlers, Set secretKeyHandlerFactories, + String mappingsInfoClassName, Set mappings, Set configCustomizers, Set configBuilders) { @@ -608,7 +691,13 @@ private static void generateConfigBuilder( for (ConfigClassWithPrefix mapping : mappings) { method.invokeStaticMethod(WITH_MAPPING, configBuilder, method.load(mapping.getKlass().getName()), method.load(mapping.getPrefix())); + ResultHandle names = method.invokeStaticMethod(MethodDescriptor.ofMethod(mappingsInfoClassName, + mapping.getKlass().getName().replaceAll("\\.", "_") + "_" + mapping.getPrefix().hashCode(), Map.class)); + method.invokeVirtualMethod(WITH_NAMES, configBuilder, names); } + ResultHandle keys = method + .invokeStaticMethod(MethodDescriptor.ofMethod(mappingsInfoClassName, "keys", Set.class)); + method.invokeVirtualMethod(WITH_KEYS, configBuilder, keys); method.returnVoid(); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/RuntimeConfigBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/RuntimeConfigBuilder.java index 4b37d9b268a3f..b8cd9691775f8 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/RuntimeConfigBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/RuntimeConfigBuilder.java @@ -13,6 +13,7 @@ public class RuntimeConfigBuilder implements SmallRyeConfigBuilderCustomizer { @Override public void configBuilder(final SmallRyeConfigBuilder builder) { new QuarkusConfigBuilderCustomizer().configBuilder(builder); + builder.withMappingDefaults(false); builder.withDefaultValue("quarkus.uuid", UUID.randomUUID().toString()); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/StaticInitConfigBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/StaticInitConfigBuilder.java index d27c3bbb1e2e2..b45028fa991d0 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/StaticInitConfigBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/StaticInitConfigBuilder.java @@ -11,6 +11,7 @@ public class StaticInitConfigBuilder implements SmallRyeConfigBuilderCustomizer @Override public void configBuilder(final SmallRyeConfigBuilder builder) { new QuarkusConfigBuilderCustomizer().configBuilder(builder); + builder.withMappingDefaults(false); builder.forClassLoader(Thread.currentThread().getContextClassLoader()) .addDefaultInterceptors() diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java index 10bcd6bcabb3f..37cff3614d779 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java @@ -1,20 +1,19 @@ package io.quarkus.gradle.tasks; +import static io.smallrye.config.SmallRyeConfigBuilder.META_INF_MICROPROFILE_CONFIG_PROPERTIES; import static java.util.Collections.*; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Consumer; import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSourceProvider; @@ -23,10 +22,12 @@ import io.quarkus.deployment.configuration.ClassLoadingConfig; import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.runtime.configuration.ApplicationPropertiesConfigSourceLoader; import io.quarkus.runtime.configuration.ConfigUtils; import io.smallrye.config.AbstractLocationConfigSourceLoader; import io.smallrye.config.EnvConfigSource; import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.PropertiesConfigSourceProvider; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.common.utils.ConfigSourceUtil; import io.smallrye.config.source.yaml.YamlConfigSource; @@ -43,8 +44,6 @@ final class EffectiveConfig { private final Map fullConfig; - // URLs of all application.properties/yaml/yml files that were consulted (including those that do not exist) - private final List applicationPropsSources; private final SmallRyeConfig config; private EffectiveConfig(Builder builder) { @@ -60,11 +59,10 @@ private EffectiveConfig(Builder builder) { // 300 -> System.getenv() // 290 -> quarkusBuildProperties // 280 -> projectProperties - // 250 -> application.(properties|yaml|yml) (in classpath/source) + // 255 -> application,(yaml|yml) (in classpath/source) + // 250 -> application.properties (in classpath/source) // 100 -> microprofile.properties (in classpath/source) - applicationPropsSources = new ArrayList<>(); - configSources.add(new PropertiesConfigSource(builder.forcedProperties, "forcedProperties", 600)); configSources.add(new PropertiesConfigSource(asStringMap(builder.taskProperties), "taskProperties", 500)); configSources.add(new PropertiesConfigSource(ConfigSourceUtil.propertiesToMap(System.getProperties()), @@ -74,18 +72,15 @@ private EffectiveConfig(Builder builder) { configSources.add(new PropertiesConfigSource(builder.buildProperties, "quarkusBuildProperties", 290)); configSources.add(new PropertiesConfigSource(asStringMap(builder.projectProperties), "projectProperties", 280)); - configSourcesForApplicationProperties(builder.sourceDirectories, applicationPropsSources::add, configSources::add, 250, - new String[] { - "application.properties", - "application.yaml", - "application.yml" - }); - configSourcesForApplicationProperties(builder.sourceDirectories, applicationPropsSources::add, configSources::add, 100, - new String[] { - "microprofile-config.properties" - }); - this.config = buildConfig(builder.profile, configSources); + ClassLoader classLoader = toUrlClassloader(builder.sourceDirectories); + ApplicationPropertiesConfigSourceLoader.InClassPath applicationProperties = new ApplicationPropertiesConfigSourceLoader.InClassPath(); + configSources.addAll(applicationProperties.getConfigSources(classLoader)); + ApplicationYamlConfigSourceLoader.InClassPath applicationYaml = new ApplicationYamlConfigSourceLoader.InClassPath(); + configSources.addAll(applicationYaml.getConfigSources(classLoader)); + configSources + .addAll(PropertiesConfigSourceProvider.classPathSources(META_INF_MICROPROFILE_CONFIG_PROPERTIES, classLoader)); + this.config = buildConfig(builder.profile, configSources); this.fullConfig = generateFullConfigMap(config); } @@ -135,35 +130,6 @@ Map configMap() { return fullConfig; } - List applicationPropsSources() { - return applicationPropsSources; - } - - static void configSourcesForApplicationProperties(Set sourceDirectories, Consumer sourceUrls, - Consumer configSourceConsumer, int ordinal, String[] fileExtensions) { - for (var sourceDir : sourceDirectories) { - var sourceDirPath = sourceDir.toPath(); - var locations = new ArrayList(); - for (String file : fileExtensions) { - Path resolved = sourceDirPath.resolve(file); - if (Files.exists(resolved)) { - locations.add(resolved.toUri().toString()); - } - } - if (!locations.isEmpty()) { - URLClassLoader classLoader; - try { - classLoader = new URLClassLoader(new URL[] { sourceDir.toURI().toURL() }); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - CombinedConfigSourceProvider configSourceProvider = new CombinedConfigSourceProvider(sourceUrls, ordinal, - fileExtensions, locations); - configSourceProvider.getConfigSources(classLoader).forEach(configSourceConsumer); - } - } - } - static final class Builder { private Map buildProperties = emptyMap(); private Map projectProperties = emptyMap(); @@ -207,34 +173,46 @@ Builder withProfile(String profile) { } } - static final class CombinedConfigSourceProvider extends AbstractLocationConfigSourceLoader implements ConfigSourceProvider { - private final Consumer sourceUrls; - private final int ordinal; - private final String[] fileExtensions; - private final List locations; - - CombinedConfigSourceProvider(Consumer sourceUrls, int ordinal, String[] fileExtensions, List locations) { - this.sourceUrls = sourceUrls; - this.ordinal = ordinal; - this.fileExtensions = fileExtensions; - this.locations = locations; + private static ClassLoader toUrlClassloader(Set sourceDirectories) { + List urls = new ArrayList<>(); + for (File sourceDirectory : sourceDirectories) { + try { + urls.add(sourceDirectory.toURI().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } } + return new URLClassLoader(urls.toArray(new URL[0])); + } + // Copied from quarkus-config-yaml. May be replaced by adding the quarkus-config-yaml dependency + public static class ApplicationYamlConfigSourceLoader extends AbstractLocationConfigSourceLoader { @Override protected String[] getFileExtensions() { - return fileExtensions; + return new String[] { + "yaml", + "yml" + }; } @Override protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException { - sourceUrls.accept(url); - return url.getPath().endsWith(".properties") ? new PropertiesConfigSource(url, ordinal) - : new YamlConfigSource(url, ordinal); + return new YamlConfigSource(url, ordinal); } - @Override - public List getConfigSources(final ClassLoader classLoader) { - return loadConfigSources(locations.toArray(new String[0]), ordinal, classLoader); + public static class InClassPath extends ApplicationYamlConfigSourceLoader implements ConfigSourceProvider { + @Override + public List getConfigSources(final ClassLoader classLoader) { + List configSources = new ArrayList<>(); + configSources.addAll(loadConfigSources("application.yaml", 255, classLoader)); + configSources.addAll(loadConfigSources("application.yml", 255, classLoader)); + return configSources; + } + + @Override + protected List tryFileSystem(final URI uri, final int ordinal) { + return new ArrayList<>(); + } } } } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java index 7d1e51492be9e..cd37b8b1aad86 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusShowEffectiveConfig.java @@ -6,8 +6,8 @@ import java.io.BufferedWriter; import java.io.File; import java.io.IOException; -import java.net.URL; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; @@ -48,7 +48,8 @@ public void dumpEffectiveConfiguration() { EffectiveConfig effective = extension() .buildEffectiveConfiguration(extension().getApplicationModel().getAppArtifact()); Map configMap = effective.configMap(); - List applicationPropsSources = effective.applicationPropsSources(); + List sourceNames = new ArrayList<>(); + effective.config().getConfigSources().forEach(configSource -> sourceNames.add(configSource.getName())); String config = configMap.entrySet().stream() .filter(e -> e.getKey().startsWith("quarkus.")) @@ -74,8 +75,7 @@ public void dumpEffectiveConfiguration() { fastJar, runnerJar(), nativeRunner(), - applicationPropsSources.stream().map(Object::toString) - .collect(Collectors.joining("\n ", "\n ", "\n"))); + sourceNames.stream().collect(Collectors.joining("\n ", "\n ", "\n"))); if (getSaveConfigProperties().get()) { Properties props = new Properties(); diff --git a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/tasks/EffectiveConfigTest.java b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/tasks/EffectiveConfigTest.java index a07b71be11d10..ad239fe7fc39f 100644 --- a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/tasks/EffectiveConfigTest.java +++ b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/tasks/EffectiveConfigTest.java @@ -1,8 +1,5 @@ package io.quarkus.gradle.tasks; -import static io.quarkus.gradle.tasks.EffectiveConfig.*; -import static java.util.Collections.singleton; - import java.io.File; import java.net.URL; import java.util.ArrayList; @@ -15,11 +12,8 @@ import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.junit.jupiter.InjectSoftAssertions; import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; -import org.eclipse.microprofile.config.spi.ConfigSource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import io.smallrye.config.SmallRyeConfig; @@ -55,43 +49,6 @@ void fromForcedProperties() { soft.assertThat(effectiveConfig.configMap()).containsEntry("quarkus.foo", "bar"); } - @ParameterizedTest - @ValueSource(strings = { - "app-props-and-yaml", - "app-props-and-yaml-and-yml", - "app-yaml-and-yml", - "single-app-props", - "single-app-yaml", - "single-app-yml" - }) - void appProps(String variant) throws Exception { - URL url = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/effectiveConfig/" + variant); - List urls = new ArrayList<>(); - List configSources = new ArrayList<>(); - configSourcesForApplicationProperties(singleton(new File(url.toURI())), urls::add, configSources::add, 250, - new String[] { - "application.properties", - "application.yaml", - "application.yml" - }); - SmallRyeConfig config = buildConfig("prod", configSources); - - Map expected = new HashMap<>(); - if (variant.contains("-yml")) { - expected.put("quarkus.prop.yml", "yml"); - } - if (variant.contains("-yaml")) { - expected.put("quarkus.prop.yaml", "yaml"); - } - if (variant.contains("-props")) { - expected.put("quarkus.prop.properties", "hello"); - } - - soft.assertThat(urls).hasSize(expected.size() * 2); // "no profile" + "prod" profile - - soft.assertThat(generateFullConfigMap(config)).containsAllEntriesOf(expected); - } - @Test void appPropsOverload() throws Exception { URL url1 = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/effectiveConfig/overload/1/"); @@ -102,12 +59,13 @@ void appPropsOverload() throws Exception { EffectiveConfig effectiveConfig = EffectiveConfig.builder().withSourceDirectories(source).build(); - soft.assertThat(effectiveConfig.applicationPropsSources()).containsExactly( - url1.toURI().resolve("application.properties").toURL(), - url2.toURI().resolve("application.yaml").toURL(), - url1.toURI().resolve("application-prod.properties").toURL(), - url2.toURI().resolve("application-prod.yaml").toURL()); - soft.assertThat(effectiveConfig.configMap()).containsEntry("quarkus.prop.overload", "overloaded"); + SmallRyeConfig config = effectiveConfig.config(); + List sourceNames = new ArrayList<>(); + config.getConfigSources().forEach(configSource -> sourceNames.add(configSource.getName())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url1.getPath())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url2.getPath())); + // The YAML source is always higher in ordinal than the properties source + soft.assertThat(effectiveConfig.configMap()).containsEntry("quarkus.prop.overload", "from-yaml"); } @Test @@ -122,14 +80,14 @@ void appPropsOverloadWrongProfile() throws Exception { EffectiveConfig effectiveConfig = EffectiveConfig.builder().withSourceDirectories(source).build(); - soft.assertThat(effectiveConfig.applicationPropsSources()).containsExactly( - url1.toURI().resolve("application.properties").toURL(), - url2.toURI().resolve("application.yaml").toURL(), - url3.toURI().resolve("application.properties").toURL(), - url1.toURI().resolve("application-prod.properties").toURL(), - url2.toURI().resolve("application-prod.yaml").toURL(), - url3.toURI().resolve("application-prod.properties").toURL()); - soft.assertThat(effectiveConfig.configMap()).containsEntry("quarkus.prop.overload", "overloaded"); + SmallRyeConfig config = effectiveConfig.config(); + List sourceNames = new ArrayList<>(); + config.getConfigSources().forEach(configSource -> sourceNames.add(configSource.getName())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url1.getPath())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url2.getPath())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url3.getPath())); + // The YAML source is always higher in ordinal than the properties source + soft.assertThat(effectiveConfig.configMap()).containsEntry("quarkus.prop.overload", "from-yaml"); } @Test @@ -138,23 +96,25 @@ void appPropsOverloadProdProfile() throws Exception { URL url2 = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/effectiveConfig/overload/2/"); URL url3 = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/effectiveConfig/overload/3/"); URL url4 = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/effectiveConfig/overload/4/"); + URL url5 = getClass().getClassLoader().getResource("io/quarkus/gradle/tasks/effectiveConfig/overload/5/"); Set source = new LinkedHashSet<>(); - source.add(new File(url4.toURI())); source.add(new File(url1.toURI())); source.add(new File(url2.toURI())); source.add(new File(url3.toURI())); + source.add(new File(url4.toURI())); + source.add(new File(url5.toURI())); EffectiveConfig effectiveConfig = EffectiveConfig.builder().withSourceDirectories(source).build(); - soft.assertThat(effectiveConfig.applicationPropsSources()).containsExactly( - url4.toURI().resolve("application.properties").toURL(), - url1.toURI().resolve("application.properties").toURL(), - url2.toURI().resolve("application.yaml").toURL(), - url3.toURI().resolve("application.properties").toURL(), - url4.toURI().resolve("application-prod.properties").toURL(), - url1.toURI().resolve("application-prod.properties").toURL(), - url2.toURI().resolve("application-prod.yaml").toURL(), - url3.toURI().resolve("application-prod.properties").toURL()); - soft.assertThat(effectiveConfig.configMap()).containsEntry("quarkus.prop.overload", "but-this-one"); + SmallRyeConfig config = effectiveConfig.config(); + List sourceNames = new ArrayList<>(); + config.getConfigSources().forEach(configSource -> sourceNames.add(configSource.getName())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url1.getPath())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url2.getPath())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url3.getPath())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url4.getPath())); + soft.assertThat(sourceNames).anyMatch(s -> s.contains(url5.getPath())); + // The YAML source is always higher in ordinal than the properties source, even for profile property names + soft.assertThat(effectiveConfig.configMap()).containsEntry("quarkus.prop.overload", "from-yaml-prod"); } } diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.properties b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.properties deleted file mode 100644 index f6834e346f911..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -quarkus.prop.properties=hello -quarkus.package.type=fast-jar diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.yaml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.yaml deleted file mode 100644 index 8f45de2459e8a..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.yaml +++ /dev/null @@ -1,5 +0,0 @@ -quarkus: - prop: - yaml: 'yaml' - package: - type: legacy-jar diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.yml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.yml deleted file mode 100644 index 9c537d34e6b77..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml-and-yml/application.yml +++ /dev/null @@ -1,5 +0,0 @@ -quarkus: - prop: - yml: 'yml' - package: - type: legacy diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml/application.properties b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml/application.properties deleted file mode 100644 index cfe085a3e1b51..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml/application.properties +++ /dev/null @@ -1 +0,0 @@ -quarkus.prop.properties=hello diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml/application.yaml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml/application.yaml deleted file mode 100644 index 5cb44a30844f9..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-props-and-yaml/application.yaml +++ /dev/null @@ -1,3 +0,0 @@ -quarkus: - prop: - yaml: 'yaml' diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-yaml-and-yml/application.yaml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-yaml-and-yml/application.yaml deleted file mode 100644 index 5cb44a30844f9..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-yaml-and-yml/application.yaml +++ /dev/null @@ -1,3 +0,0 @@ -quarkus: - prop: - yaml: 'yaml' diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-yaml-and-yml/application.yml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-yaml-and-yml/application.yml deleted file mode 100644 index df2aa80f08d2c..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/app-yaml-and-yml/application.yml +++ /dev/null @@ -1,3 +0,0 @@ -quarkus: - prop: - yml: 'yml' diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/1/application.properties b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/1/application.properties index 7c111e47ae34c..f84e5addee7e0 100644 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/1/application.properties +++ b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/1/application.properties @@ -1 +1 @@ -quarkus.prop.overload=overloaded +quarkus.prop.overload=from-properties diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/2/application.yaml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/2/application.yaml index 4a0e14d38751d..fa5f171d735c2 100644 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/2/application.yaml +++ b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/2/application.yaml @@ -1,3 +1,3 @@ quarkus: prop: - overload: 'not-this' + overload: 'from-yaml' diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/4/application.properties b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/4/application.properties index 8729402ad91a1..0f530e8be82da 100644 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/4/application.properties +++ b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/4/application.properties @@ -1 +1 @@ -%prod.quarkus.prop.overload=but-this-one +%prod.quarkus.prop.overload=from-properties-prod-profile diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/5/application-prod.yaml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/5/application-prod.yaml new file mode 100644 index 0000000000000..128eb836274b0 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/5/application-prod.yaml @@ -0,0 +1,4 @@ +"%prod": + quarkus: + prop: + overload: 'from-yaml-prod' diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/5/application.yaml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/5/application.yaml new file mode 100644 index 0000000000000..fa5f171d735c2 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/overload/5/application.yaml @@ -0,0 +1,3 @@ +quarkus: + prop: + overload: 'from-yaml' diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-props/application.properties b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-props/application.properties deleted file mode 100644 index cfe085a3e1b51..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-props/application.properties +++ /dev/null @@ -1 +0,0 @@ -quarkus.prop.properties=hello diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-yaml/application.yaml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-yaml/application.yaml deleted file mode 100644 index 5cb44a30844f9..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-yaml/application.yaml +++ /dev/null @@ -1,3 +0,0 @@ -quarkus: - prop: - yaml: 'yaml' diff --git a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-yml/application.yml b/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-yml/application.yml deleted file mode 100644 index df2aa80f08d2c..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/test/resources/io/quarkus/gradle/tasks/effectiveConfig/single-app-yml/application.yml +++ /dev/null @@ -1,3 +0,0 @@ -quarkus: - prop: - yml: 'yml' diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigPropertyMapInjectionTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigPropertyMapInjectionTest.java index ff2c75589e47a..b590a8a791d4c 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigPropertyMapInjectionTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigPropertyMapInjectionTest.java @@ -26,7 +26,7 @@ public class ConfigPropertyMapInjectionTest { "root.numbers.1=one\n" + "root.numbers.2=two\n" + "root.numbers.3=three\n" + - "versions.v1=1.The version 1.2.3\n" + + "versions.v1=1.The version 1.0.3\n" + "versions.v1.2=1.The version 1.2.0\n" + "versions.v2=2.The version 2.0.0\n"), "application.properties")); @@ -73,10 +73,12 @@ void mapInjection() { assertEquals("two", sNumbers.get().get(2)); assertEquals("three", sNumbers.get().get(3)); - assertEquals(2, versions.size()); - assertEquals(new Version(1, "The version 1.2.3"), versions.get("v1")); + assertEquals(3, versions.size()); + assertEquals(new Version(1, "The version 1.0.3"), versions.get("v1")); + assertEquals(new Version(1, "The version 1.2.0"), versions.get("v1.2")); assertEquals(new Version(2, "The version 2.0.0"), versions.get("v2")); - assertEquals(2, versionsDefault.size()); + assertEquals(3, versionsDefault.size()); + assertEquals(new Version(0, "The version 0"), versionsDefault.get("v0.1")); assertEquals(new Version(1, "The version 1;2;3"), versionsDefault.get("v1=1;2;3")); assertEquals(new Version(2, "The version 2;1;0"), versionsDefault.get("v2=2;1;0")); } From d7c765ca12e88ce02111efade1644d16e00cca4b Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 3 Jan 2024 13:03:28 +0200 Subject: [PATCH 27/36] Add native test for verifying JsonSubTypes.Type handling --- .../it/jackson/JsonSubTypesResource.java | 29 +++++++++++++++++++ .../io/quarkus/it/jackson/model/Elephant.java | 5 ++++ .../io/quarkus/it/jackson/model/Mammal.java | 15 ++++++++++ .../it/jackson/model/MammalFamily.java | 9 ++++++ .../io/quarkus/it/jackson/model/Whale.java | 5 ++++ .../it/jackson/JsonSubTypesResourceIT.java | 7 +++++ .../it/jackson/JsonSubTypesResourceTest.java | 27 +++++++++++++++++ 7 files changed, 97 insertions(+) create mode 100644 integration-tests/jackson/src/main/java/io/quarkus/it/jackson/JsonSubTypesResource.java create mode 100644 integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Elephant.java create mode 100644 integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Mammal.java create mode 100644 integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/MammalFamily.java create mode 100644 integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Whale.java create mode 100644 integration-tests/jackson/src/test/java/io/quarkus/it/jackson/JsonSubTypesResourceIT.java create mode 100644 integration-tests/jackson/src/test/java/io/quarkus/it/jackson/JsonSubTypesResourceTest.java diff --git a/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/JsonSubTypesResource.java b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/JsonSubTypesResource.java new file mode 100644 index 0000000000000..ebffa6018819c --- /dev/null +++ b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/JsonSubTypesResource.java @@ -0,0 +1,29 @@ +package io.quarkus.it.jackson; + +import java.util.List; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.it.jackson.model.Elephant; +import io.quarkus.it.jackson.model.MammalFamily; +import io.quarkus.it.jackson.model.Whale; + +@Path("jsonSubTypes") +public class JsonSubTypesResource { + + private final ObjectMapper objectMapper; + + public JsonSubTypesResource(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @GET + public String test() throws JsonProcessingException { + return objectMapper.writeValueAsString(new MammalFamily(List.of( + new Whale(30.0, "white"), new Elephant(10, "africa")))); + } +} diff --git a/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Elephant.java b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Elephant.java new file mode 100644 index 0000000000000..7fe29940824d7 --- /dev/null +++ b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Elephant.java @@ -0,0 +1,5 @@ +package io.quarkus.it.jackson.model; + +public record Elephant( + int hornLength, String continent) implements Mammal { +} diff --git a/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Mammal.java b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Mammal.java new file mode 100644 index 0000000000000..dc622346349f2 --- /dev/null +++ b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Mammal.java @@ -0,0 +1,15 @@ +package io.quarkus.it.jackson.model; + +import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY; +import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = NAME, include = PROPERTY) +@JsonSubTypes({ + @JsonSubTypes.Type(value = Elephant.class, name = "Elephant"), + @JsonSubTypes.Type(value = Whale.class, name = "Whale"), +}) +public interface Mammal { +} diff --git a/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/MammalFamily.java b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/MammalFamily.java new file mode 100644 index 0000000000000..60192c223f1fa --- /dev/null +++ b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/MammalFamily.java @@ -0,0 +1,9 @@ +package io.quarkus.it.jackson.model; + +import java.util.Collection; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +public record MammalFamily(Collection mammals) { +} diff --git a/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Whale.java b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Whale.java new file mode 100644 index 0000000000000..f19fc797c0756 --- /dev/null +++ b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/Whale.java @@ -0,0 +1,5 @@ +package io.quarkus.it.jackson.model; + +public record Whale( + double swimSpeed, String color) implements Mammal { +} diff --git a/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/JsonSubTypesResourceIT.java b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/JsonSubTypesResourceIT.java new file mode 100644 index 0000000000000..9d58d36a156f4 --- /dev/null +++ b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/JsonSubTypesResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.jackson; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class JsonSubTypesResourceIT extends JsonSubTypesResourceTest { +} diff --git a/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/JsonSubTypesResourceTest.java b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/JsonSubTypesResourceTest.java new file mode 100644 index 0000000000000..fe1979b72db7a --- /dev/null +++ b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/JsonSubTypesResourceTest.java @@ -0,0 +1,27 @@ +package io.quarkus.it.jackson; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import io.restassured.response.Response; + +@QuarkusTest +class JsonSubTypesResourceTest { + + @Test + void test() { + Response response = given() + .accept(ContentType.JSON) + .when() + .get("jsonSubTypes") + .then() + .statusCode(200) + .extract().response(); + assertThat(response.jsonPath().getString("mammals[0].color")).isEqualTo("white"); + assertThat(response.jsonPath().getString("mammals[1].continent")).isEqualTo("africa"); + } +} From 4f239039bb297cb07ee063888bf5722d07d9e987 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 3 Jan 2024 12:43:03 +0200 Subject: [PATCH 28/36] Mention exit handler parameter variant of Quarkus.run Fixes: #37929 --- docs/src/main/asciidoc/lifecycle.adoc | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/src/main/asciidoc/lifecycle.adoc b/docs/src/main/asciidoc/lifecycle.adoc index 911f10ff02f8b..a528863ac6bf0 100644 --- a/docs/src/main/asciidoc/lifecycle.adoc +++ b/docs/src/main/asciidoc/lifecycle.adoc @@ -111,6 +111,42 @@ public class Main { } ---- +[TIP] +==== +`Quarkus.run` also provides a version that allows the code to handle errors. +For example: + +[source,java] +---- +package com.acme; + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.runtime.annotations.QuarkusMain; + +@QuarkusMain +public class Main { + public static void main(String... args) { + Quarkus.run(MyApp.class, + (exitCode, exception) -> { + // do whatever + }, + args); + } + + public static class MyApp implements QuarkusApplication { + + @Override + public int run(String... args) throws Exception { + System.out.println("Do startup logic here"); + Quarkus.waitForExit(); + return 0; + } + } +} +---- +==== + === Injecting the command line arguments It is possible to inject the arguments that were passed in on the command line: From 13662011f190c1de6e736000e52360839d3bb82d Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 3 Jan 2024 15:01:22 +0200 Subject: [PATCH 29/36] Fix the status is a couple extensions documentation pages Closes: #37941 --- docs/src/main/asciidoc/cache.adoc | 2 -- docs/src/main/asciidoc/security-oauth2.adoc | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/src/main/asciidoc/cache.adoc b/docs/src/main/asciidoc/cache.adoc index 2b711ed7ff187..10f397041d653 100644 --- a/docs/src/main/asciidoc/cache.adoc +++ b/docs/src/main/asciidoc/cache.adoc @@ -12,8 +12,6 @@ include::_attributes.adoc[] In this guide, you will learn how to enable application data caching in any CDI managed bean of your Quarkus application. -include::{includes}/extension-status.adoc[] - == Prerequisites include::{includes}/prerequisites.adoc[] diff --git a/docs/src/main/asciidoc/security-oauth2.adoc b/docs/src/main/asciidoc/security-oauth2.adoc index 7720edb8bb238..cf216a14c8225 100644 --- a/docs/src/main/asciidoc/security-oauth2.adoc +++ b/docs/src/main/asciidoc/security-oauth2.adoc @@ -22,8 +22,6 @@ This extension provides a light-weight support for using the opaque Bearer Token If the OAuth2 Authentication server provides JWT Bearer Tokens, consider using either xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication] or xref:security-jwt.adoc[SmallRye JWT] extensions instead. OpenID Connect extension has to be used if the Quarkus application needs to authenticate the users using OIDC Authorization Code Flow. For more information, see the xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications] guide. -include::{includes}/extension-status.adoc[] - == Solution We recommend that you follow the instructions in the next sections and create the application step by step. From 04d4a2cf0b01c01da18d7f3c5b2854f9c6e5cac9 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Wed, 3 Jan 2024 15:08:30 +0100 Subject: [PATCH 30/36] Remove keytabs created in test-resources, the test now copies them to the target dir Fixes #38002 --- .../src/main/resources/application.properties | 2 +- .../quarkus/it/kafka/KafkaSaslTestResource.java | 8 ++++---- .../it/kafka/containers/KerberosContainer.java | 17 +++++++++-------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/integration-tests/kafka-sasl-elytron/src/main/resources/application.properties b/integration-tests/kafka-sasl-elytron/src/main/resources/application.properties index 8b0689bc04437..0fa8b998798d3 100644 --- a/integration-tests/kafka-sasl-elytron/src/main/resources/application.properties +++ b/integration-tests/kafka-sasl-elytron/src/main/resources/application.properties @@ -4,7 +4,7 @@ quarkus.log.category.\"org.apache.zookeeper\".level=WARN mp.messaging.connector.smallrye-kafka.security.protocol=SASL_PLAINTEXT mp.messaging.connector.smallrye-kafka.sasl.mechanism=GSSAPI -mp.messaging.connector.smallrye-kafka.sasl.jaas.config=com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true storeKey=true debug=true serviceName="kafka" keyTab="src/test/resources/client.keytab" principal="client/localhost@EXAMPLE.COM"; +mp.messaging.connector.smallrye-kafka.sasl.jaas.config=com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true storeKey=true debug=true serviceName="kafka" keyTab="target/client.keytab" principal="client/localhost@EXAMPLE.COM"; mp.messaging.connector.smallrye-kafka.sasl.kerberos.service.name=kafka mp.messaging.connector.smallrye-kafka.ssl.endpoint.identification.algorithm=https diff --git a/integration-tests/kafka-sasl-elytron/src/test/java/io/quarkus/it/kafka/KafkaSaslTestResource.java b/integration-tests/kafka-sasl-elytron/src/test/java/io/quarkus/it/kafka/KafkaSaslTestResource.java index 3e44f45793636..f496581ecaaa5 100644 --- a/integration-tests/kafka-sasl-elytron/src/test/java/io/quarkus/it/kafka/KafkaSaslTestResource.java +++ b/integration-tests/kafka-sasl-elytron/src/test/java/io/quarkus/it/kafka/KafkaSaslTestResource.java @@ -6,7 +6,6 @@ import java.util.Map; import org.jboss.logging.Logger; -import org.testcontainers.containers.BindMode; import org.testcontainers.utility.MountableFile; import io.quarkus.it.kafka.containers.KerberosContainer; @@ -39,9 +38,10 @@ public Map start() { c -> String.format("SASL_PLAINTEXT://%s:%s", c.getHost(), c.getMappedPort(KAFKA_PORT))) .withPort(KAFKA_PORT) .withServerProperties(MountableFile.forClasspathResource("kafkaServer.properties")) - .withCopyFileToContainer(MountableFile.forClasspathResource("krb5KafkaBroker.conf"), "/etc/krb5.conf") - .withFileSystemBind("src/test/resources/kafkabroker.keytab", "/opt/kafka/config/kafkabroker.keytab", - BindMode.READ_ONLY); + .withCopyFileToContainer(MountableFile.forClasspathResource("krb5KafkaBroker.conf"), + "/etc/krb5.conf") + .withCopyFileToContainer(MountableFile.forHostPath("target/kafkabroker.keytab"), + "/opt/kafka/config/kafkabroker.keytab"); kafka.start(); log.info(kafka.getLogs()); properties.put("kafka.bootstrap.servers", kafka.getBootstrapServers()); diff --git a/integration-tests/kafka-sasl-elytron/src/test/java/io/quarkus/it/kafka/containers/KerberosContainer.java b/integration-tests/kafka-sasl-elytron/src/test/java/io/quarkus/it/kafka/containers/KerberosContainer.java index 79eac8a49b55f..022e3523ce24e 100644 --- a/integration-tests/kafka-sasl-elytron/src/test/java/io/quarkus/it/kafka/containers/KerberosContainer.java +++ b/integration-tests/kafka-sasl-elytron/src/test/java/io/quarkus/it/kafka/containers/KerberosContainer.java @@ -7,7 +7,6 @@ import java.time.Duration; import java.util.stream.Collectors; -import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; @@ -21,21 +20,23 @@ public KerberosContainer(String dockerImageName) { withEnv("KRB5_KDC", "localhost"); withEnv("KRB5_PASS", "mypass"); withExposedPorts(749, 464, 88); - withFileSystemBind("src/test/resources/kafkabroker.keytab", "/tmp/keytab/kafkabroker.keytab", BindMode.READ_WRITE); - withFileSystemBind("src/test/resources/client.keytab", "/tmp/keytab/client.keytab", BindMode.READ_WRITE); - waitingFor(Wait.forLogMessage("Principal \"admin/admin@EXAMPLE.COM\" created.*", 1)); + waitingFor(Wait.forListeningPorts(88)); withNetwork(Network.SHARED); withNetworkAliases("kerberos"); } public void createTestPrincipals() { try { - ExecResult lsResult = execInContainer("kadmin.local", "-q", "addprinc -randkey kafka/localhost@EXAMPLE.COM"); + ExecResult lsResult = execInContainer("kadmin.local", "-q", + "addprinc -randkey kafka/localhost@EXAMPLE.COM"); lsResult = execInContainer("kadmin.local", "-q", - "ktadd -norandkey -k /tmp/keytab/kafkabroker.keytab kafka/localhost@EXAMPLE.COM"); - lsResult = execInContainer("kadmin.local", "-q", "addprinc -randkey client/localhost@EXAMPLE.COM"); + "ktadd -norandkey -k /kafkabroker.keytab kafka/localhost@EXAMPLE.COM"); lsResult = execInContainer("kadmin.local", "-q", - "ktadd -norandkey -k /tmp/keytab/client.keytab client/localhost@EXAMPLE.COM"); + "addprinc -randkey client/localhost@EXAMPLE.COM"); + lsResult = execInContainer("kadmin.local", "-q", + "ktadd -norandkey -k /client.keytab client/localhost@EXAMPLE.COM"); + copyFileFromContainer("/kafkabroker.keytab", "target/kafkabroker.keytab"); + copyFileFromContainer("/client.keytab", "target/client.keytab"); } catch (Exception e) { e.printStackTrace(); } From 5bd6a7947a1ac31f6aca711db3952f31eb8ea78e Mon Sep 17 00:00:00 2001 From: "rob.spoor" Date: Wed, 3 Jan 2024 16:00:38 +0100 Subject: [PATCH 31/36] Set the correct port properties for HTTPS --- .../http/runtime/PortSystemProperties.java | 38 +++++--- .../it/vertx/RandomTestPortTestCase.java | 86 +++++++++++++++++++ 2 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/RandomTestPortTestCase.java diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PortSystemProperties.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PortSystemProperties.java index 40fa135373900..02d3a1bf5deb5 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PortSystemProperties.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PortSystemProperties.java @@ -11,28 +11,38 @@ public class PortSystemProperties { public void set(String subProperty, int actualPort, LaunchMode launchMode) { String portPropertyValue = String.valueOf(actualPort); - //we always set the .port property, even if we are in test mode, so this will always - //reflect the current port String portPropertyName = "quarkus." + subProperty + ".port"; - String prevPortPropertyValue = System.setProperty(portPropertyName, portPropertyValue); - if (!Objects.equals(prevPortPropertyValue, portPropertyValue)) { - portPropertiesToRestore.put(portPropertyName, prevPortPropertyValue); + String testPropName = "quarkus." + subProperty + ".test-port"; + + set(portPropertyName, testPropName, portPropertyValue, launchMode); + //if subProperty is "https", the correct properties are not quarkus.https.port and quarkus.https.test-port + //but quarkus.http.ssl-port and quarkus.http.test-ssl-port + //the incorrect properties are still set for backward compatibility with code that works around the incorrect + //names + if ("https".equals(subProperty)) { + set("quarkus.http.ssl-port", "quarkus.http.test-ssl-port", portPropertyValue, launchMode); } + } + + private void set(String portPropertyName, String testPropName, String portPropertyValue, LaunchMode launchMode) { + //we always set the .port property, even if we are in test mode, so this will always + //reflect the current port + set(portPropertyName, portPropertyValue); if (launchMode == LaunchMode.TEST) { //we also set the test-port property in a test - String testPropName = "quarkus." + subProperty + ".test-port"; - String prevTestPropPrevValue = System.setProperty(testPropName, portPropertyValue); - if (!Objects.equals(prevTestPropPrevValue, portPropertyValue)) { - portPropertiesToRestore.put(testPropName, prevTestPropPrevValue); - } + set(testPropName, portPropertyValue); } if (launchMode.isDevOrTest()) { // set the profile property as well to make sure we don't have any inconsistencies portPropertyName = "%" + launchMode.getDefaultProfile() + "." + portPropertyName; - prevPortPropertyValue = System.setProperty(portPropertyName, portPropertyValue); - if (!Objects.equals(prevPortPropertyValue, portPropertyValue)) { - portPropertiesToRestore.put(portPropertyName, prevPortPropertyValue); - } + set(portPropertyName, portPropertyValue); + } + } + + private void set(String propertyName, String propertyValue) { + String prevPropertyValue = System.setProperty(propertyName, propertyValue); + if (!Objects.equals(prevPropertyValue, propertyValue)) { + portPropertiesToRestore.put(propertyName, prevPropertyValue); } } diff --git a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/RandomTestPortTestCase.java b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/RandomTestPortTestCase.java new file mode 100644 index 0000000000000..b8275f241d538 --- /dev/null +++ b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/RandomTestPortTestCase.java @@ -0,0 +1,86 @@ +package io.quarkus.it.vertx; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.net.URL; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(RandomTestPortTestCase.TestPortProfile.class) +public class RandomTestPortTestCase { + + @ConfigProperty(name = "quarkus.http.port") + int httpPort; + + @ConfigProperty(name = "quarkus.http.test-port") + int httpTestPort; + + @TestHTTPResource(value = "/some-path") + URL httpTestUrl; + + @ConfigProperty(name = "quarkus.http.ssl-port") + int httpsPort; + + @ConfigProperty(name = "quarkus.http.test-ssl-port") + int httpsTestPort; + + @TestHTTPResource(value = "/some-path", ssl = true) + URL httpsTestUrl; + + @Test + public void testHttpTestPort() { + assertNotEquals(0, httpTestPort); + assertNotEquals(8080, httpTestPort); + assertNotEquals(8081, httpTestPort); + + assertEquals(httpTestPort, httpPort); + assertEquals(httpTestPort, httpTestUrl.getPort()); + } + + @Test + public void testHttpsTestPort() { + assertNotEquals(0, httpsTestPort); + assertNotEquals(8443, httpsTestPort); + assertNotEquals(8444, httpsTestPort); + + assertEquals(httpsTestPort, httpsPort); + assertEquals(httpsTestPort, httpsTestUrl.getPort()); + } + + @Test + public void testLegacyProperties() { + Config config = ConfigProvider.getConfig(); + + testLegacyProperty(config, "quarkus.http.ssl-port", "quarkus.https.port", httpsPort); + testLegacyProperty(config, "quarkus.http.test-ssl-port", "quarkus.https.test-port", httpsTestPort); + testLegacyProperty(config, "%test.quarkus.http.ssl-port", "%test.quarkus.https.port", httpsTestPort); + } + + private void testLegacyProperty(Config config, String correctName, String legacyName, int expectedValue) { + Optional portWithCorrectProperty = config.getOptionalValue(correctName, Integer.class); + Optional portWithLegacyProperty = config.getOptionalValue(legacyName, Integer.class); + + assertEquals(Optional.of(expectedValue), portWithCorrectProperty); + assertEquals(portWithCorrectProperty, portWithLegacyProperty); + } + + public static class TestPortProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.http.test-port", "0", "quarkus.http.test-ssl-port", "0"); + } + } +} From 221f8b9d916a0f2bd0ebfb0e539a2766afc8ccef Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Wed, 3 Jan 2024 16:06:03 +0100 Subject: [PATCH 32/36] Display Json output before injecting build scans Makes debugging a lot easier. --- .github/workflows/develocity-publish-build-scans.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml index 7aded6de65891..05f242298964a 100644 --- a/.github/workflows/develocity-publish-build-scans.yml +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -28,6 +28,13 @@ jobs: develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} skip-comment: true skip-summary: true + - name: Output JSON file + run: | + if [ -f "${{ steps.publish.outputs.build-metadata-file-path }}" ]; then + echo "```json" >> $GITHUB_STEP_SUMMARY + jq '.' ${{ steps.publish.outputs.build-metadata-file-path }} >> $GITHUB_STEP_SUMMARY + echo "\n```" >> $GITHUB_STEP_SUMMARY; + fi - name: Inject build scans in reports uses: quarkusio/action-helpers@main with: @@ -35,7 +42,3 @@ jobs: action: inject-build-scans workflow-run-id: ${{ github.event.workflow_run.id }} build-metadata-file-path: ${{ steps.publish.outputs.build-metadata-file-path }} - - name: Output JSON file - run: | - if [ -f "${{ steps.publish.outputs.build-metadata-file-path }}" ]; then jq '.' ${{ steps.publish.outputs.build-metadata-file-path }} >> $GITHUB_STEP_SUMMARY; fi - From 2401bd6493336b2d967612b4c6ac57a986192678 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 3 Jan 2024 11:49:45 +0100 Subject: [PATCH 33/36] ArC: improve description of injection points that are parameters The improvement is twofold: - it highlights that the injection point is a parameter by putting the parameter name at the beginning instead of the end; - it uses the word "constructor" explicitly in case the parameter belongs to a constructor. --- .../quarkus/qute/deployment/inject/InjectionFailedTest.java | 2 +- .../java/io/quarkus/arc/processor/InjectionPointInfo.java | 6 +++--- .../illegal/TypeVariableInitializerInjectionPointTest.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectionFailedTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectionFailedTest.java index 3cb2b28440fe8..f054c9482ba5f 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectionFailedTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectionFailedTest.java @@ -37,7 +37,7 @@ public class InjectionFailedTest { assertTrue(s.getMessage().contains( "No template found for path [foo] defined at io.quarkus.qute.deployment.inject.InjectionFailedTest#foo") || s.getMessage().contains( - "No template found for path [alpha] defined at io.quarkus.qute.deployment.inject.InjectionFailedTest$Client()"), + "No template found for path [alpha] defined at parameter 'alpha' of io.quarkus.qute.deployment.inject.InjectionFailedTest$Client constructor"), s.getMessage()); } }); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java index 3174b2976989f..e79adfdaa8da4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java @@ -292,11 +292,11 @@ public String getTargetInfo() { } String method = target.asMethod().name(); if (method.equals(Methods.INIT)) { - method = ""; + method = " constructor"; } else { - method = "#" + method; + method = "#" + method + "()"; } - return target.asMethod().declaringClass().name() + method + "()" + ":" + param; + return "parameter '" + param + "' of " + target.asMethod().declaringClass().name() + method; default: return target.toString(); } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/illegal/TypeVariableInitializerInjectionPointTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/illegal/TypeVariableInitializerInjectionPointTest.java index a82cd637dce02..76224e6c4b102 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/illegal/TypeVariableInitializerInjectionPointTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/illegal/TypeVariableInitializerInjectionPointTest.java @@ -24,7 +24,7 @@ public void testError() { assertNotNull(failure); assertTrue(failure instanceof DefinitionException); assertEquals( - "Type variable is not a legal injection point type: io.quarkus.arc.test.injection.illegal.TypeVariableInitializerInjectionPointTest$Head#setIt():it", + "Type variable is not a legal injection point type: parameter 'it' of io.quarkus.arc.test.injection.illegal.TypeVariableInitializerInjectionPointTest$Head#setIt()", failure.getMessage()); } From 9d5ea193a42af779db6ea6c3d3ef64b804028f4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 20:46:35 +0000 Subject: [PATCH 34/36] Bump org.assertj:assertj-core from 3.25.0 to 3.25.1 in /devtools/gradle Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.25.0 to 3.25.1. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.25.0...assertj-build-3.25.1) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- devtools/gradle/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index e5a3d641a7415..34743ae4dcaa7 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -6,7 +6,7 @@ kotlin = "1.9.22" smallrye-config = "3.4.4" junit5 = "5.10.1" -assertj = "3.25.0" +assertj = "3.25.1" [plugins] plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "plugin-publish" } From 890e5edfe909032f36221967e0344c5d3909b069 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 22:45:59 +0000 Subject: [PATCH 35/36] Bump com.fasterxml.jackson:jackson-bom from 2.16.0 to 2.16.1 Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.16.0 to 2.16.1. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.16.0...jackson-bom-2.16.1) --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- independent-projects/extension-maven-plugin/pom.xml | 2 +- independent-projects/resteasy-reactive/pom.xml | 2 +- independent-projects/tools/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 01a8a68f0764f..a56482ebd0529 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -91,7 +91,7 @@ 2.1.0 23.0.1 1.7.0 - 2.16.0 + 2.16.1 1.0.0.Final 3.14.0 1.16.0 diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index ffe4b85c6aea7..3b5329fa1dd6a 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -42,7 +42,7 @@ 3.2.1 3.2.3 3.10.2 - 2.16.0 + 2.16.1 1.3.2 5.10.1 diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 06620402f69af..8105e0084d879 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -64,7 +64,7 @@ 4.4.6 5.4.0 1.0.0.Final - 2.16.0 + 2.16.1 2.4.0 3.0.2 3.0.3 diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index 1479e6f49aaa5..439d9689dcf8c 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -49,7 +49,7 @@ 3.24.2 - 2.16.0 + 2.16.1 4.0.1 5.10.1 1.25.0 From 10f7ed5056c61e56cf8e7207a8d00a4da1b83666 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 22:47:37 +0000 Subject: [PATCH 36/36] Bump org.asciidoctor:asciidoctorj from 2.5.10 to 2.5.11 Bumps [org.asciidoctor:asciidoctorj](https://github.com/asciidoctor/asciidoctorj) from 2.5.10 to 2.5.11. - [Release notes](https://github.com/asciidoctor/asciidoctorj/releases) - [Changelog](https://github.com/asciidoctor/asciidoctorj/blob/main/CHANGELOG.adoc) - [Commits](https://github.com/asciidoctor/asciidoctorj/compare/v2.5.10...v2.5.11) --- updated-dependencies: - dependency-name: org.asciidoctor:asciidoctorj dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-parent/pom.xml b/build-parent/pom.xml index c1f53169a6996..50f8af63b4426 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -36,7 +36,7 @@ 3.1.6 1.0.0 - 2.5.10 + 2.5.11 2.70.0 3.25.6 2.0.3.Final