diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesNetworkIdBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesNetworkIdBuildItem.java index 88dfd8c6ad0bc..df11d9773fa50 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesNetworkIdBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesNetworkIdBuildItem.java @@ -4,6 +4,11 @@ /** * The network id of the network that the dev services are running on. + * This is intended to be consumed in the IntegrationTest launcher to ensure that the application is running on the same network + * as the dev services. + *

+ * In the future if extensions consume this build item, it would create the shared network if it doesn't exist, + * and use it for the dev services and the test containers. */ public final class DevServicesNetworkIdBuildItem extends SimpleBuildItem { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesRegistryBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesRegistryBuildItem.java index bda6efd2ef0f1..b9ceca195a2f2 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesRegistryBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesRegistryBuildItem.java @@ -184,21 +184,26 @@ private void reallyStart(DevServicesResultBuildItem request, List config = request.getConfig(startable); + Map overrideConfig = request.getOverrideConfig(startable); + configs.putAll(config); + configs.putAll(overrideConfig); + // We do not "copy" the config map here since it is created within the request.getConfig: - Map combinedConfig = request.getConfig(startable); // Some extensions may rely on adding/overriding config properties // depending on the results of the started dev services, // e.g. Hibernate Search/ORM may change the default schema management // if it detects that it runs over a dev service datasource/Elasticsearch distribution. for (DevServicesAdditionalConfigBuildItem additionalConfigBuildItem : additionalConfigBuildItems) { Map extraFromBuildItem = additionalConfigBuildItem.getConfigProvider() - .provide(combinedConfig); + .provide(configs); if (!extraFromBuildItem.isEmpty()) { - combinedConfig.putAll(extraFromBuildItem); + configs.putAll(extraFromBuildItem); } } RunningService service = new RunningService(request.getName(), request.getDescription(), - combinedConfig, request.getOverrideConfig(startable), startable.getContainerId(), startable); + configs, request.getOverrideConfig(startable), startable.getContainerId(), startable); this.addRunningService(request.getName(), request.getServiceName(), request.getServiceConfig(), service); compressor.close(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JvmStartupOptimizerArchiveContainerImageBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JvmStartupOptimizerArchiveContainerImageBuildItem.java index d1e43c5ab710d..9ed1b164256df 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JvmStartupOptimizerArchiveContainerImageBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JvmStartupOptimizerArchiveContainerImageBuildItem.java @@ -1,19 +1,34 @@ package io.quarkus.deployment.pkg.builditem; +import java.util.List; +import java.util.Optional; + import io.quarkus.builder.item.SimpleBuildItem; /** * Indicates that a specific container image should be used to generate the AppCDS file */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public final class JvmStartupOptimizerArchiveContainerImageBuildItem extends SimpleBuildItem { private final String containerImage; + private final Optional> additionalJvmArgs; + @Deprecated(forRemoval = true, since = "3.34") public JvmStartupOptimizerArchiveContainerImageBuildItem(String containerImage) { + this(containerImage, Optional.empty()); + } + + public JvmStartupOptimizerArchiveContainerImageBuildItem(String containerImage, Optional> additionalJvmArgs) { this.containerImage = containerImage; + this.additionalJvmArgs = additionalJvmArgs; } public String getContainerImage() { return containerImage; } + + public Optional> getAdditionalJvmArgs() { + return additionalJvmArgs; + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/jar/AbstractFastJarBuilder.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/jar/AbstractFastJarBuilder.java index ac98efe497d9d..74148de79ef03 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/jar/AbstractFastJarBuilder.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/jar/AbstractFastJarBuilder.java @@ -213,7 +213,7 @@ public JarBuildItem build() throws IOException { if (!rebuild) { manifestConfig.addComponent(ApplicationComponent.builder() - .setResolvedDependency(applicationArchives.getRootArchive().getResolvedDependency()) + .setResolvedDependency(appArtifact) .setPath(runnerJar)); Predicate ignoredEntriesPredicate = getThinJarIgnoredEntriesPredicate(packageConfig); try (ArchiveCreator archiveCreator = new ParallelCommonsCompressArchiveCreator(runnerJar, @@ -271,6 +271,7 @@ public JarBuildItem build() throws IOException { runnerJar.toFile().setReadable(true, false); Path initJar = buildDir.resolve(FastJarFormat.QUARKUS_RUN_JAR); manifestConfig.setMainComponent(ApplicationComponent.builder() + .setVersion(appArtifact.getVersion()) .setPath(initJar) .setDependencies(List.of(curateOutcome.getApplicationModel().getAppArtifact()))) .setRunnerPath(initJar); @@ -285,7 +286,10 @@ public JarBuildItem build() throws IOException { List lines = Arrays.stream(out.toString(StandardCharsets.UTF_8).split("\n")) .filter(s -> !s.startsWith("#")).sorted().collect(Collectors.toList()); Path buildSystemProps = quarkus.resolve(FastJarFormat.BUILD_SYSTEM_PROPERTIES); - manifestConfig.addComponent(ApplicationComponent.builder().setPath(buildSystemProps).setDevelopmentScope()); + manifestConfig.addComponent(ApplicationComponent.builder() + .setVersion(appArtifact.getVersion()) + .setPath(buildSystemProps) + .setDevelopmentScope()); try (OutputStream fileOutput = Files.newOutputStream(buildSystemProps)) { fileOutput.write(String.join("\n", lines).getBytes(StandardCharsets.UTF_8)); } @@ -323,7 +327,10 @@ public JarBuildItem build() throws IOException { curateOutcome.getApplicationModel(), packageConfig.jar().userProvidersDirectory().orElse(null), buildDir.relativize(runnerJar).toString()); Path appmodelDat = deploymentLib.resolve(FastJarFormat.APPMODEL_DAT); - manifestConfig.addComponent(ApplicationComponent.builder().setPath(appmodelDat).setDevelopmentScope()); + manifestConfig.addComponent(ApplicationComponent.builder() + .setVersion(appArtifact.getVersion()) + .setPath(appmodelDat) + .setDevelopmentScope()); try (OutputStream out = Files.newOutputStream(appmodelDat)) { ObjectOutputStream obj = new ObjectOutputStream(out); obj.writeObject(model); @@ -334,7 +341,10 @@ public JarBuildItem build() throws IOException { //as we don't really have a resolved bootstrap CP //once we have the app model it will all be done in QuarkusClassLoader anyway Path deploymentCp = deploymentLib.resolve(FastJarFormat.DEPLOYMENT_CLASS_PATH_DAT); - manifestConfig.addComponent(ApplicationComponent.builder().setPath(deploymentCp).setDevelopmentScope()); + manifestConfig.addComponent(ApplicationComponent.builder() + .setVersion(appArtifact.getVersion()) + .setPath(deploymentCp) + .setDevelopmentScope()); try (OutputStream out = Files.newOutputStream(deploymentCp)) { ObjectOutputStream obj = new ObjectOutputStream(out); List paths = new ArrayList<>(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JvmStartupOptimizerArchiveBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JvmStartupOptimizerArchiveBuildStep.java index 3677f6f3387cb..c2b3073cd7904 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JvmStartupOptimizerArchiveBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JvmStartupOptimizerArchiveBuildStep.java @@ -169,8 +169,15 @@ public void build(Optional request archivePath = createAppCDSFromExit(jarResult, outputTarget, javaBinPath, containerImage, isFastJar); } else if (archiveType == JvmStartupOptimizerArchiveType.AOT) { - archivePath = createAot(jarResult, outputTarget, javaBinPath, containerImage, isFastJar, - packageConfig.jar().aot().additionalRecordingArgs().orElse(List.of())); + List additionalJvmArguments = new ArrayList<>(); + if (packageConfig.jar().aot().additionalRecordingArgs().isPresent()) { + additionalJvmArguments.addAll(packageConfig.jar().aot().additionalRecordingArgs().get()); + } + if (jvmStartupOptimizerArchiveContainerImage.isPresent() + && jvmStartupOptimizerArchiveContainerImage.get().getAdditionalJvmArgs().isPresent()) { + additionalJvmArguments.addAll(jvmStartupOptimizerArchiveContainerImage.get().getAdditionalJvmArgs().get()); + } + archivePath = createAot(jarResult, outputTarget, javaBinPath, containerImage, isFastJar, additionalJvmArguments); } else { throw new IllegalStateException("Unsupported archive type: " + archiveType); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 757a27d7b342d..d3947bd185b5f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -104,6 +104,7 @@ ArtifactResultBuildItem result(NativeImageBuildItem image, ApplicationManifestConfig.builder() .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) .setMainComponent(ApplicationComponent.builder() + .setVersion(curateOutcomeBuildItem.getApplicationModel().getAppArtifact().getVersion()) .setPath(image.getPath()) .setDependencies(List.of(curateOutcomeBuildItem.getApplicationModel().getAppArtifact()))) .setRunnerPath(image.getPath()) @@ -184,6 +185,7 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, ApplicationManifestConfig.builder() .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) .setMainComponent(ApplicationComponent.builder() + .setVersion(curateOutcomeBuildItem.getApplicationModel().getAppArtifact().getVersion()) .setPath(nativeImageSourceJarBuildItem.getPath()) .setResolvedDependency(curateOutcomeBuildItem.getApplicationModel().getAppArtifact())) .setRunnerPath(nativeImageSourceJarBuildItem.getPath()) diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestFastJarTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestFastJarTest.java index 524f4e4e989b7..d649658a3231e 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestFastJarTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestFastJarTest.java @@ -6,6 +6,7 @@ import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.sbom.ApplicationComponent; public class ApplicationManifestFastJarTest extends ApplicationManifestTestBase { @@ -15,7 +16,7 @@ protected TsArtifact composeApplication() { var acmeTransitive = TsArtifact.jar("acme-transitive"); - var acmeCommon = TsArtifact.jar("acme-common") + var acmeCommon = TsArtifact.jar("acme-common", "3.0") .addDependency(acmeTransitive); var acmeLib = TsArtifact.jar("acme-lib") @@ -31,7 +32,7 @@ protected TsArtifact composeApplication() { myExt.getRuntime().addDependency(myLib); myExt.getDeployment().addDependency(otherLib); - return TsArtifact.jar("app") + return TsArtifact.jar("app", "2.0") .addManagedDependency(platformDescriptor()) .addManagedDependency(platformProperties()) .addDependency(acmeLib) @@ -49,8 +50,9 @@ protected Properties buildSystemProperties() { @BeforeEach public void initExpectedComponents() { - expectMavenComponent(artifactCoords("app"), comp -> { + expectMavenComponent(ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "app", "2.0"), comp -> { assertDistributionPath(comp, "app/quarkus-application.jar"); + assertVersion(comp, "2.0"); assertDependencies(comp, artifactCoords("acme-lib"), artifactCoords("other-lib"), @@ -59,64 +61,77 @@ public void initExpectedComponents() { assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); + final ArtifactCoords commonsCoords = ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "acme-common", "3.0"); + expectMavenComponent(artifactCoords("acme-lib"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.acme-lib-1.jar"); - assertDependencies(comp, artifactCoords("acme-common")); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); + assertDependencies(comp, commonsCoords); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); - expectMavenComponent(artifactCoords("acme-common"), comp -> { - assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.acme-common-1.jar"); + expectMavenComponent(commonsCoords, comp -> { + assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.acme-common-3.0.jar"); + assertVersion(comp, "3.0"); assertDependencies(comp, artifactCoords("acme-transitive")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("acme-transitive"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.acme-transitive-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("other-lib"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.other-lib-1.jar"); - assertDependencies(comp, artifactCoords("acme-common")); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); + assertDependencies(comp, commonsCoords); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-lib"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.my-lib-1.jar"); - assertDependencies(comp, artifactCoords("acme-common")); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); + assertDependencies(comp, commonsCoords); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-ext"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.my-ext-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("my-lib")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus-run.jar", comp -> { - assertDependencies(comp, artifactCoords("app")); + assertVersion(comp, "2.0"); + assertDependencies(comp, ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "app", "2.0")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus/generated-bytecode.jar", comp -> { + assertVersion(comp, "2.0"); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus/quarkus-application.dat", comp -> { + assertVersion(comp, "2.0"); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus-app-dependencies.txt", comp -> { + assertVersion(comp, "2.0"); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-ext-deployment"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("my-ext"), artifactCoords("other-lib")); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestMutableJarTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestMutableJarTest.java index 31ea0e13a2f2a..640c778a6e30b 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestMutableJarTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestMutableJarTest.java @@ -50,6 +50,7 @@ protected Properties buildSystemProperties() { public void initExpectedComponents() { expectMavenComponent(artifactCoords("app"), comp -> { assertDistributionPath(comp, "app/quarkus-application.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-lib"), artifactCoords("other-lib"), @@ -60,67 +61,79 @@ public void initExpectedComponents() { expectMavenComponent(artifactCoords("acme-lib"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.acme-lib-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-common")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("acme-common"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.acme-common-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-transitive")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("acme-transitive"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.acme-transitive-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("other-lib"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.other-lib-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-common")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-lib"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.my-lib-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-common")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-ext"), comp -> { assertDistributionPath(comp, "lib/main/io.quarkus.bootstrap.test.my-ext-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("my-lib")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus-run.jar", comp -> { + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("app")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus/generated-bytecode.jar", comp -> { + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus/quarkus-application.dat", comp -> { + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus-app-dependencies.txt", comp -> { + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("quarkus/build-system.properties", comp -> { + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-ext-deployment"), comp -> { assertDistributionPath(comp, "lib/deployment/io.quarkus.bootstrap.test.my-ext-deployment-1.jar"); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencyScope(comp, ApplicationComponent.SCOPE_DEVELOPMENT); assertDependencies(comp, artifactCoords("my-ext"), @@ -128,11 +141,13 @@ public void initExpectedComponents() { }); expectFileComponent("lib/deployment/appmodel.dat", comp -> { + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectFileComponent("lib/deployment/deployment-class-path.dat", comp -> { + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestTestBase.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestTestBase.java index b22edc1ad2eb0..a87478c846ccb 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestTestBase.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestTestBase.java @@ -75,6 +75,12 @@ protected static void assertNoDistributionPath(ApplicationComponent comp) { .as(() -> ApplicationManifestTestBase.getComponentKey(comp) + " is not found in the distribution").isNull(); } + protected static void assertVersion(ApplicationComponent comp, String expectedVersion) { + assertThat(comp.getVersion()) + .as(() -> ApplicationManifestTestBase.getComponentKey(comp) + " has version") + .isEqualTo(expectedVersion); + } + protected static void assertDependencies(ApplicationComponent comp, ArtifactCoords... expectedDeps) { assertThat(toArtifactCoordsList(comp.getDependencies())) .as(() -> ApplicationManifestTestBase.getComponentKey(comp) + " has dependencies") diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestUberJarTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestUberJarTest.java index 3a9b7d1d55eb5..46eec79a5be96 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestUberJarTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ApplicationManifestUberJarTest.java @@ -50,6 +50,7 @@ protected Properties buildSystemProperties() { public void initExpectedComponents() { expectMavenComponent(artifactCoords("app", "runner"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-lib"), artifactCoords("other-lib"), @@ -60,42 +61,49 @@ public void initExpectedComponents() { expectMavenComponent(artifactCoords("acme-lib"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-common")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("acme-common"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-transitive")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("acme-transitive"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("other-lib"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-common")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-lib"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("acme-common")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-ext"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("my-lib")); assertDependencyScope(comp, ApplicationComponent.SCOPE_RUNTIME); }); expectMavenComponent(artifactCoords("my-ext-deployment"), comp -> { assertNoDistributionPath(comp); + assertVersion(comp, TsArtifact.DEFAULT_VERSION); assertDependencies(comp, artifactCoords("my-ext"), artifactCoords("other-lib")); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractConfigBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractConfigBuilder.java index 1815961a28736..5da77bee93c87 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractConfigBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractConfigBuilder.java @@ -31,7 +31,7 @@ public abstract class AbstractConfigBuilder implements SmallRyeConfigBuilderCustomizer { protected static void withSharedBuilder(SmallRyeConfigBuilder builder) { - builder.withMappingIgnore("quarkus.**"); + builder.addDefaultInterceptors().withCustomizers(new QuarkusConfigBuilderCustomizer()); } protected static void withDefaultValues(SmallRyeConfigBuilder builder, Map values) { diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/actions/BeforeTestAction.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/actions/BeforeTestAction.java index 1aa29327a25dc..60bdd4a9c0de9 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/actions/BeforeTestAction.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/actions/BeforeTestAction.java @@ -21,6 +21,7 @@ import io.quarkus.bootstrap.BootstrapConstants; import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.gradle.tasks.EffectiveConfig; import io.quarkus.gradle.tasks.EffectiveConfigProvider; import io.quarkus.gradle.tasks.QuarkusPluginExtensionView; import io.quarkus.gradle.tooling.ToolingUtils; @@ -63,10 +64,12 @@ public void execute(Task t) { final Path serializedModel = applicationModelPath.get().getAsFile().toPath(); ApplicationModel applicationModel = ToolingUtils.deserializeAppModel(serializedModel); - SmallRyeConfig config = effectiveProvider().buildEffectiveConfiguration(applicationModel, new HashMap<>()) - .getConfig(); + EffectiveConfig effectiveConfig = effectiveProvider().buildEffectiveConfiguration(applicationModel, + new HashMap<>()); + SmallRyeConfig config = effectiveConfig.getConfig(); config.getOptionalValue(TEST.getProfileKey(), String.class) .ifPresent(value -> props.put(TEST.getProfileKey(), value)); + props.putAll(effectiveConfig.getQuarkusValues()); props.put(BootstrapConstants.SERIALIZED_TEST_APP_MODEL, serializedModel.toString()); diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc index d7406c9a28473..329fb21d7f629 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/src/main/asciidoc/_attributes.adoc @@ -90,14 +90,17 @@ :create-app-group-id: org.acme :create-cli-group-id: {create-app-group-id} // Attributes required for single-sourcing to downstream. -:jdk-version-other: 17 -:jdk-version-latest: 21 -:jdk-version-all: 17 or 21 +:jdk-version-earliest: 17 +:jdk-version-other: 21 +:jdk-version-latest: 25 +:jdk-version-all: 17, 21, or 25 :openshift-long: OpenShift :openshift: OpenShift :name-image-ubi9-open-jdk-17: registry.access.redhat.com/ubi9/openjdk-17 :name-image-ubi9-open-jdk-17-short: ubi9/openjdk-17 :name-image-ubi9-open-jdk-21: registry.access.redhat.com/ubi9/openjdk-21 :name-image-ubi9-open-jdk-21-short: ubi9/openjdk-21 +:name-image-ubi9-open-jdk-25: registry.access.redhat.com/ubi9/openjdk-25 +:name-image-ubi9-open-jdk-25-short: ubi9/openjdk-25 // . include::_attributes-local.adoc[] diff --git a/docs/src/main/asciidoc/aot.adoc b/docs/src/main/asciidoc/aot.adoc index 029342e0fb393..94045d954687c 100644 --- a/docs/src/main/asciidoc/aot.adoc +++ b/docs/src/main/asciidoc/aot.adoc @@ -173,6 +173,44 @@ You can obtain more information about AOT cache usage with the `-Xlog:aot` optio You can collect even more information in an `aot.log` file with `-Xlog:class+load=info,aot+codecache=debug:file=aot.log:level,tags`. ==== +== Manual AOT file generation + +As explained above, Quarkus takes care of all the heavy lifting when coming to the AOT file generation. However, in cases where more control is needed, users can perform the necessary steps manually. + +To build an application using the `aot-jar` packaging, use the following: + +[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-maven"] +.Maven +---- +mvn package -Dquarkus.package.jar.type=aot-jar +---- + +[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +.Gradle +---- +./gradlew build -Dquarkus.package.jar.type=aot-jar +---- + +To start the application in recording mode, use the following: + +[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-maven"] +.Maven +---- +cd target/quarkus-app +java -XX:AOTCacheOutput=app.aot -jar quarkus-run.jar +---- + +[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +.Gradle +---- +cd build/quarkus-app +java -XX:AOTCacheOutput=app.aot-jar quarkus-run.jar +---- + +After driving some load to the application, stop it. Once the process is stopped, you will notice that the `app.aot` has been created in the same directory as `quarkus-run.jar`. + +Please refer to [JEP 514](https://openjdk.org/jeps/514) for more details. + == Container images Quarkus can automatically build container images that include AOT caches, giving you fast startup times out of the box. diff --git a/docs/src/main/asciidoc/deploying-to-openshift-s2i-howto.adoc b/docs/src/main/asciidoc/deploying-to-openshift-s2i-howto.adoc index 665f2c4601365..347562ebf0c2b 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift-s2i-howto.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift-s2i-howto.adoc @@ -24,31 +24,27 @@ To deploy {project-name} applications compiled to native executables, use the Do ==== endif::no-S2I-support[] -The deployment procedure differs based on the Java version your {project-name} application uses. +You can deploy {project-name} applications to {openshift} with Java {jdk-version-all} versions. -[[using-the-S2I-Java-17]] -== Deploying {project-name} applications to {openshift} with Java {jdk-version-other} +== Prerequisites -You can deploy {project-name} applications that run Java {jdk-version-other} to {openshift} by using the S2I method. - -=== Prerequisites - -* You have a Quarkus application built with Java {jdk-ver-other}. +* You have a Quarkus application built with Java {jdk-version-all}. * Optional: You have a Quarkus project that includes the `quarkus-openshift` extension. * You are working in the correct OpenShift project namespace. * Your project is hosted in a Git repository. -=== Procedure +== Procedure -. Open the `pom.xml` file, and set the Java version to {jdk-version-other}: +. Open the `pom.xml` file, and set the Java version. + [source,xml,subs=attributes+] ---- -{jdk-version-other} -{jdk-version-other} +${java.version} +${java.version} ---- +where ${java.version} is {jdk-version-all}. + -. Package your Java {jdk-version-other} application, by entering the following command: +. Package your application, by entering the following command: + [source,shell] ---- @@ -68,136 +64,69 @@ JAVA_APP_JAR=/deployments/quarkus-run.jar + . Commit and push your changes to the remote Git repository. -. Import the supported {openshift} image by entering the following command: +. Import the supported {openshift} image. ++ +Java {jdk-version-earliest}: + [source,subs="attributes+,+quotes"] ---- oc import-image {name-image-ubi9-open-jdk-17-short} --from={name-image-ubi9-open-jdk-17} --confirm ---- +Java {jdk-version-other}: + -[NOTE] -==== -* If you are using the OpenShift image registry and pulling from image streams in the same project, your pod service account must already have the correct permissions. -* If you are pulling images across other {openshift} projects or from secured registries, additional configuration steps might be required. - -For more information, see the link:https://docs.openshift.com/container-platform/[Red Hat Openshift Container Platform] documentation. -==== - -. Build the project, create the application, and deploy the {openshift} service: -+ -[source,xml,subs="attributes+,+quotes"] +[source,subs="attributes+,+quotes"] ---- -oc new-app registry.access.redhat.com/ubi9/openjdk-17~ --name= +oc import-image {name-image-ubi9-open-jdk-21-short} --from={name-image-ubi9-open-jdk-21} --confirm ---- + -* Replace `` with the path of the Git repository that hosts your Quarkus project. -For example, `oc new-app registry.access.redhat.com/ubi9/openjdk-17~https://github.com/johndoe/code-with-quarkus.git --name=code-with-quarkus`. -+ -If you do not have SSH keys configured for the Git repository, when specifying the Git path, use the HTTPS URL instead of the SSH URL. - -* Replace `` with the name of your application. - -. To deploy an updated version of the project, push changes to the Git repository, and then run: +Java {jdk-version-latest}: + -[source,xml,subs="attributes+,+quotes"] +[source,subs="attributes+,+quotes"] ---- -oc start-build +oc import-image {name-image-ubi9-open-jdk-25-short} --from={name-image-ubi9-open-jdk-25} --confirm ---- + -. To expose a route to the application, run the following command: -+ -[source,shell,subs="attributes+,+quotes"] ----- -oc expose svc ----- - - -=== Verification +[NOTE] +==== +* If you are using the OpenShift image registry and pulling from image streams in the same project, your pod service account must already have the correct permissions. -. List the pods associated with your current {openshift} project: -+ -[source,shell,subs="attributes+,+quotes"] ----- -oc get pods ----- -. To get the log output for your application's pod, run the following command, replacing `` with the name of the latest pod prefixed by your application name: +* If you are pulling images across other {openshift} projects or from secured registries, additional configuration steps might be required. + -[source,shell,subs="attributes+,+quotes"] ----- -oc logs -f ----- - -== Deploying {project-name} applications to {openshift} with Java {jdk-version-latest} - -You can deploy {project-name} applications that run Java {jdk-version-latest} to {openshift} by using the S2I method. - -=== Prerequisites - -* Optional: You have a Quarkus Maven project that includes the `quarkus-openshift` extension. -* You are working in the correct {openshift} project namespace. -* Your project is hosted in a Git repository. +For more information, see the link:https://docs.openshift.com/container-platform/[Red Hat Openshift Container Platform] documentation. -=== Procedure +* If you are deploying on IBM Z infrastructure, enter `oc import-image {name-image-ubi9-open-jdk-21-short} --from=registry.redhat.io/{name-image-ubi9-open-jdk-21-short} --confirm` instead. +For information about this image, see link:https://catalog.redhat.com/en/software/containers/ubi9/openjdk-21-runtime/6501ce769a0d86945c422d5f[OpenJDK 21 runtime image on UBI9]. +==== -. Open the `pom.xml` file, and set the Java version to {jdk-version-latest}: -+ -[source,xml,subs=attributes+] ----- -{jdk-version-latest} -{jdk-version-latest} ----- -+ -. Package your Java {jdk-ver-latest} application, by entering the following command: +. Build the project, create the application, and deploy the {openshift} service. + -[source,shell] ----- -./mvnw clean package ----- -. Create a directory called `.s2i` at the same level as the `pom.xml` file. -. Create a file called `environment` in the `.s2i` directory and add the following content: +Java {jdk-version-earliest}: + -[source] +[source,xml,subs="attributes+,+quotes"] ---- -MAVEN_S2I_ARTIFACT_DIRS=target/quarkus-app -S2I_SOURCE_DEPLOYMENTS_FILTER=app lib quarkus quarkus-run.jar -JAVA_OPTIONS=-Dquarkus.http.host=0.0.0.0 -AB_JOLOKIA_OFF=true -JAVA_APP_JAR=/deployments/quarkus-run.jar +oc new-app registry.access.redhat.com/ubi9/openjdk-17~ --name= ---- -. Commit and push your changes to the remote Git repository. -. Import the supported {openshift} image by entering the following command: +Java {jdk-version-other}: + -[source,subs="attributes+,+quotes"] +[source,xml,subs="attributes+,+quotes"] ---- -oc import-image {name-image-ubi9-open-jdk-21-short} --from={name-image-ubi9-open-jdk-21} --confirm +oc new-app registry.access.redhat.com/ubi9/openjdk-21~ --name= ---- + -[NOTE] -==== -* If you are using the OpenShift image registry and pulling from image streams in the same project, your pod service account must already have the correct permissions. - -* If you are pulling images across other {openshift} projects or from secured registries, additional configuration steps might be required. -For more information, see the link:https://docs.openshift.com/container-platform/[Red Hat Openshift Container Platform] documentation. - -* If you are deploying on IBM Z infrastructure, enter `oc import-image {name-image-ubi9-open-jdk-21-short} --from=registry.redhat.io/{name-image-ubi9-open-jdk-21-short} --confirm` instead. -For information about this image, see link:https://catalog.redhat.com/software/containers/ubi9/openjdk-21/653fb7e21b2ec10f7dfc10d0[{runtimes-openjdk-long} 21]. -==== - -. Build the project, create the application, and deploy the {openshift} service: +Java {jdk-version-latest}: + [source,xml,subs="attributes+,+quotes"] ---- -oc new-app registry.access.redhat.com/ubi8/openjdk-21~ --name= +oc new-app registry.access.redhat.com/ubi9/openjdk-25~ --name= ---- + * Replace `` with the path of the Git repository that hosts your Quarkus project. -For example, `oc new-app registry.access.redhat.com/ubi9/openjdk-21~https://github.com/johndoe/code-with-quarkus.git --name=code-with-quarkus`. +For example, for Java {jdk-version-other}: `oc new-app registry.access.redhat.com/ubi9/openjdk-21~https://github.com/johndoe/code-with-quarkus.git --name=code-with-quarkus`. + If you do not have SSH keys configured for the Git repository, when specifying the Git path, use the HTTPS URL instead of the SSH URL. * Replace `` with the name of your application. + - [NOTE] ==== If you are deploying on IBM Z infrastructure, enter `oc new-app ubi9/openjdk-21~ --name=` instead. @@ -209,7 +138,7 @@ If you are deploying on IBM Z infrastructure, enter `oc new-app ubi9/openjdk-21~ ---- oc start-build ---- - ++ . To expose a route to the application, run the following command: + [source,shell,subs="attributes+,+quotes"] @@ -217,7 +146,7 @@ oc start-build oc expose svc ---- -=== Verification +== Verification . List the pods associated with your current {openshift} project: + @@ -225,7 +154,6 @@ oc expose svc ---- oc get pods ---- -+ . To get the log output for your application's pod, run the following command, replacing `` with the name of the latest pod prefixed by your application name: + [source,shell,subs="attributes+,+quotes"] diff --git a/docs/src/main/asciidoc/deploying-to-openshift.adoc b/docs/src/main/asciidoc/deploying-to-openshift.adoc index d0b30a09c8082..84465274c121e 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift.adoc @@ -18,7 +18,7 @@ include::_attributes.adoc[] Quarkus offers the ability to automatically generate {openshift} resources based on sane defaults and user-supplied configuration. As an application developer, you can deploy your {project-name} applications to {openshift-long}. -This functionality is provided by the `quarkus-openshift` extension, which supports multiple deployment options: +The `quarkus-openshift` extension provides this functionality, which supports multiple deployment options: * xref:deploying-to-openshift-howto.adoc[With a single step] * xref:deploying-to-openshift-docker-howto.adoc[By using a Docker build strategy] @@ -43,7 +43,7 @@ Source to Image (S2I):: The build process is performed inside the {openshift} cl Binary S2I:: This strategy uses a JAR file as input to the S2I build process, which speeds up the building and deploying of your application. -=== Build strategies supported by Quarkus +=== Supported build strategies The following table outlines the build strategies that {project-name} supports: diff --git a/docs/src/main/asciidoc/tls-registry-reference.adoc b/docs/src/main/asciidoc/tls-registry-reference.adoc index 857e46a23587d..398bd9ec290e2 100644 --- a/docs/src/main/asciidoc/tls-registry-reference.adoc +++ b/docs/src/main/asciidoc/tls-registry-reference.adoc @@ -15,7 +15,8 @@ include::_attributes.adoc[] :extensions: io.quarkus:quarkus-tls-registry The TLS Registry is a Quarkus extension that centralizes TLS configuration, making it easier to manage and maintain secure connections across your application. -When defining TLS configurations in a single centralized location, you can use the TLS Registry to reference these configurations from multiple components within the application, which ensures consistency and reduces the potential for configuration errors. + +When defining TLS configurations in a single centralized location, you can use the TLS Registry to reference them from multiple components within the application, ensuring consistency and reducing the risk of configuration errors. The TLS Registry consolidates settings and supports multiple named configurations. Therefore, you can tailor TLS settings for different application parts. @@ -44,15 +45,15 @@ and compatibility with various keystore formats, such as PKCS12, PEM, and JKS. To configure a TLS connection, including key and truststores, use the `+quarkus.tls.*+` properties. These properties are required for: - * Setting up the default TLS configuration, defined directly under `+quarkus.tls.*+` - * Creating separate, named configurations by using `+quarkus.tls..*+`. +* Setting up the default TLS configuration, defined directly under `+quarkus.tls.*+` +* Creating separate, named configurations by using `+quarkus.tls..*+`. By specifying the `+quarkus.tls..*+` properties, you can adapt the TLS settings for a specific component. [IMPORTANT] ==== The default TLS configuration is not a fallback or global configuration. Each named TLS configuration, or "TLS bucket," must provide its own properties. -For instance, `quarkus.tls.reload-period` will only be applied to the default TLS configuration. +For instance, `quarkus.tls.reload-period` applies only to the default TLS configuration. ==== [IMPORTANT] @@ -61,7 +62,7 @@ As described in detail link:https://github.com/quarkusio/quarkus/blob/main/adr/0 The `quarkus.tls.trust-all` property is the only exception. ==== -=== Configuring HTTPS for a HTTP server +=== Configuring HTTPS for an HTTP server To ensure secure client-server communication, the client is often required to verify the server's authenticity. @@ -72,7 +73,7 @@ During the TLS handshake, the server presents its certificate, which the client This prevents man-in-the-middle attacks and secures data transmission. The following sections guide you through setting up HTTPS by using PEM or PKCS12 keystore types. -In addition, they provide information on how to use named configurations to specify and manage multiple TLS setups at once, which makes it possible for you to define distinct settings for each. +In addition, they provide guidance on named configurations to specify and manage multiple TLS configurations simultaneously, allowing you to define distinct settings for each. Use one of the following configuration examples based on your keystore type: @@ -150,7 +151,7 @@ quarkus.grpc.server.use-separate-server=false quarkus.grpc.server.plain-text=false ---- + -This configuration enables mTLS by ensuring that both the server and client validate each other's certificates, which provides an additional layer of security. +This configuration enables mTLS by ensuring that both the server and the client validate each other's certificates, which provides an additional layer of security. [[referencing-a-tls-configuration]] == Referencing a TLS configuration @@ -178,17 +179,22 @@ quarkus.smallrye-graphql-client.my-client.tls-configuration-name=MY_TLS_CONFIGUR [NOTE] ==== -When using the Typesafe GraphQL client with a certificate reloading mechanism, as described in the <> section, it is essential to override the bean's scope to `RequestScoped` or another similar scope shorter than the application. -This is because, by default, the Typesafe client is an application-scoped bean. -Shortening the scope guarantees that new instances of the bean created after a certificate reload will be configured with the latest certificate. -Dynamic clients are `@Dependent` scoped; inject them into components with an appropriate scope. +When you configure the Typesafe GraphQL client with certificate reloading, as described in the <> section, override the bean scope to `RequestScoped` or another scope shorter than the application scope. +The Typesafe client is an application-scoped bean by default. + +A shorter scope ensures that instances created after a certificate reload use the latest certificate. + +Dynamic clients are `@Dependent` scoped. +Inject dynamic clients into components with an appropriate scope. ==== === Referencing the default truststore of SunJSSE JDK distributions typically contain a truststore in the `$JAVA_HOME/lib/security/cacerts` file. -This truststore is used as a default truststore by SunJSSE, the default implementation of the Java Secure Socket Extension (JSSE). -SSL/TLS capabilities provided by SunJSSE are leveraged by various Java Runtime components, such as `javax.net.ssl.HttpsURLConnection` and others. +SunJSSE uses this truststore as the default truststore. +SunJSSE is the default implementation of the Java Secure Socket Extension (JSSE). + +Java runtime components, such as `javax.net.ssl.HttpsURLConnection`, rely on JSSE for SSL and TLS. Although Quarkus extensions typically do not honor the default truststore of SunJSSE, it is still practical to use it in some situations. This applies when migrating from legacy technologies or running on a Linux distribution where the SunJSSE truststore is synchronized with the operating system (OS). @@ -199,7 +205,7 @@ To simplify the use of the SunJSSE truststore, Quarkus TLS Registry provides a T * Otherwise, the paths `$JAVA_HOME/lib/security/jssecacerts` and `$JAVA_HOME/lib/security/cacerts` are checked, and the first existing file is used as a truststore. * If neither condition is met, an `IllegalStateException` is thrown. -The password for opening the truststore is taken from the `javax.net.ssl.trustStorePassword` system property. +The password to open the truststore is obtained from the `javax.net.ssl.trustStorePassword` system property. If this property is not set, the default password `changeit` is used. The `javax.net.ssl` configuration can be used as a value for various `*.tls-configuration-name` properties, as shown below: @@ -212,7 +218,7 @@ quarkus.grpc.clients.hello.tls-configuration-name=javax.net.ssl [WARNING] ==== -The `javax.net.ssl` TLS configuration can be neither customized nor overridden. +The `javax.net.ssl` TLS configuration can not be customized or overridden. ==== == Configuring TLS @@ -220,12 +226,12 @@ The `javax.net.ssl` TLS configuration can be neither customized nor overridden. TLS configuration primarily involves managing keystores and truststores. The specific setup depends on the format used, such as PEM, P12, or JKS. -The following sections outline the various properties available for configuring TLS. +The following sections outline the available TLS configuration properties. === Key stores Key stores store private keys and certificates. -They are mainly used on the server side but can also be used on the client side when mTLS is used. +They are mainly used on the server side, but can also be used on the client side when mTLS is used. ==== PEM keystores @@ -260,13 +266,13 @@ For example, `quarkus.tls.key-store.pem.order=b,c,a`. This setting is important when using SNI, because it uses the first specified pair as the default. ==== -When using PEM keystore, the following formats are supported: +When using a PEM keystore, the following formats are supported: * PKCS#8 private key (unencrypted) * PKCS#1 RSA private key (unencrypted) * Encrypted PKCS#8 private key (encrypted with AES-128-CBC) -In the later case, the `quarkus.tls.key-store.pem.password` or `quarkus.tls.key-store.pem..password` property must be set to the password used to decrypt the private key. +For an encrypted PKCS#8 private key, set the `quarkus.tls.key-store.pem.password` or `quarkus.tls.key-store.pem..password` property to the password that decrypts the private key. .An encrypted PEM keystore configuration example: [source,properties] @@ -291,7 +297,7 @@ quarkus.tls.key-store.p12.password=secret `.p12` files are password-protected, so you need to provide the password to open the keystore. -These files can include more than one certificate and private key. +These files can contain multiple certificates and private keys. If this is the case, take either of the following actions: * Provide and configure the alias of the certificate and the private key you want to use: @@ -309,7 +315,7 @@ Note that all keys must use the same password. ==== JKS keystores -JKS keystores are single files that contain the certificate and the private key for the server or client, used to authenticate and establish secure communications in TLS/SSL connections. +JKS keystores are single files that contain the server or client certificate and its private key, used to authenticate and establish secure TLS/SSL connections. [IMPORTANT] ==== @@ -328,7 +334,7 @@ quarkus.tls.key-store.jks.password=secret ---- `.jks` files are password-protected, so you need to provide the password to open the keystore. -Also, they can include more than one certificate and private key. +Also, they can include multiple certificates and private keys. If this is the case: * Provide and configure the alias of the certificate and the private key you want to use: @@ -345,7 +351,10 @@ quarkus.tls.key-store.jks.alias-password=my-alias-password Note that all keys must use the same password. ==== Provided keystores -If you need more control over the keystore used in a TLS configuration, you can provide a CDI bean implementing the `io.quarkus.tls.runtime.KeyStoreProvider` interface. Quarkus calls `KeyStoreProvider::getKeyStore` when the TLS configuration is <> and any time the configuration is <>. The resulting keystore and options are then made available via `TlsConfiguration::getKeyStore` and `TlsConfiguration::getKeyStoreOptions`. +If you need more control over the keystore used in a TLS configuration, you can provide a CDI bean implementing the `io.quarkus.tls.runtime.KeyStoreProvider` interface. + +Quarkus calls `KeyStoreProvider::getKeyStore` when the TLS configuration is <> and any time the configuration is <>. +The resulting keystore and options are then made available via `TlsConfiguration::getKeyStore` and `TlsConfiguration::getKeyStoreOptions`. .Example KeyStoreProvider [source, java] @@ -381,15 +390,18 @@ public class ExampleKeyStoreProvider implements KeyStoreProvider { } } ---- -<1> The CDI bean implementing the `KeyStoreProvider` interface can be `@ApplicationScoped`, `@Singleton` or `@Dependent`. -<2> Use the `@Identifier` qualifier to indicate a named TLS configuration for which to provide keystore options. Omit the qualifier (or use `@Default` explicitly) to indicate the default TLS configuration. See <> for more details. -<3> Other CDI beans can be injected for runtime access to keystore material. +<1> The CDI bean implementing the `KeyStoreProvider` interface can be `@ApplicationScoped`, `@Singleton`, or `@Dependent`. +<2> Use the `@Identifier` qualifier to indicate the named TLS configuration that provides the keystore options. +Omit the qualifier to indicate the default TLS configuration. +You can also use `@Default` explicitly to indicate the default TLS configuration. +For more information, see <>. +<3> You can inject other CDI beans to access keystore material at runtime. [[sni]] ==== SNI -Server Name Indication (SNI) is a TLS extension that makes it possible for a client to specify the host name to which it attempts to connect during the TLS handshake. -SNI enables a server to present different TLS certificates for multiple domains on a single IP address, which facilitates secure communication for virtual hosting scenarios. +Server Name Indication (SNI) is a TLS extension that makes it possible for a client to specify the hostname to which it attempts to connect during the TLS handshake. +SNI enables a server to present multiple TLS certificates for different domains on a single IP address, facilitating secure communication in virtual hosting scenarios. To enable SNI: @@ -404,16 +416,16 @@ With SNI enabled, the client indicates the server name during the TLS handshake, CRT is a common file extension for X.509 certificate files, typically in PEM (Privacy-Enhanced Mail) format. These files contain the public certificate. -* When configuring the keystore with a JKS or P12 file, the server selects the appropriate certificate based on the SNI host name provided by the client during the TLS handshake. +* When configuring the keystore with a JKS or P12 file, the server selects the appropriate certificate based on the SNI hostname provided by the client during the TLS handshake. The server matches the SNI hostname with the common name (CN) or subject alternative names (SAN) configured in the certificates stored in the keystore. All keystore and alias passwords must be identical. ==== Credential providers -You can use a credential provider instead of passing the keystore password and alias password in the configuration. +You can use a credential provider instead of specifying the keystore and alias passwords in the configuration. A credential provider offers a way to retrieve the keystore and alias password. -Note that the credential provider is only used if the password or alias password is not set in the configuration. +Quarkus uses the credential provider only when the configuration does not set the keystore password or alias password. To configure a credential provider: @@ -454,7 +466,7 @@ quarkus.tls.trust-store.pem.certs=ca.crt,ca2.pem ==== PKCS12 truststores -PKCS12 truststores are a single file containing the certificates. +PKCS12 truststores are single-file containers for certificates. You can use the alias to select the appropriate certificate when multiple certificates are included. To configure a PKCS12 truststore: @@ -523,9 +535,12 @@ public class ExampleTrustStoreProvider implements TrustStoreProvider { } } ---- -<1> The CDI bean implementing the `TrustStoreProvider` interface can be `@ApplicationScoped`, `@Singleton` or `@Dependent`. -<2> Use the `@Identifier` qualifier to indicate a named TLS configuration for which to provide truststore options. Omit the qualifier (or use `@Default` explicitly) to indicate the default TLS configuration. See <> for more details. -<3> Other CDI beans can be injected for runtime access to truststore material. +<1> The CDI bean that implements the `TrustStoreProvider` interface can be `@ApplicationScoped`, `@Singleton`, or `@Dependent`. +<2> Use the `@Identifier` qualifier to indicate the named TLS configuration that provides the truststore options. +Omit the qualifier to indicate the default TLS configuration. +You can also use `@Default` explicitly to indicate the default TLS configuration. +For more information, see <>. +<3> You can inject other CDI beans to access truststore material at runtime. ==== Credential providers @@ -575,7 +590,7 @@ quarkus.tls.cipher-suites=TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384 The TLS protocol versions are the list of protocols that can be used during the TLS handshake. Enabled TLS protocol versions are specified as an ordered list separated by commas. The relevant configuration property is `quarkus.tls.protocols` or `quarkus.tls..protocols` for named TLS configurations. -For improved security, it defaults to `TLSv1.3` only if not configured. +For improved security, it defaults to `TLSv1.3` unless configured otherwise. The available options are `TLSv1`, `TLSv1.1`, `TLSv1.2`, and `TLSv1.3`. @@ -610,7 +625,7 @@ quarkus.tls.handshake-timeout=10S # Default. Application-Layer Protocol Negotiation (ALPN) is a TLS extension that allows the client and server to negotiate which protocol they will use for communication during the TLS handshake. ALPN enables more efficient communication by allowing the client to indicate its preferred application protocol to the server before establishing the TLS connection. -This helps in scenarios like HTTP/2, where multiple protocols might be available, allowing for faster protocol selection. +This helps in scenarios such as HTTP/2, where multiple protocols can be available, enabling faster protocol selection. ALPN is enabled by default. @@ -632,7 +647,7 @@ However, disabling ALPN can be useful for diagnosing native inconsistencies or t A Certificate Revocation List (CRL) is a list of certificates that the issuing Certificate Authority (CA) revoked before their scheduled expiration date. When a certificate is compromised, no longer needed, or deemed invalid, the CA adds it to the CRL to inform relying parties not to trust it anymore. -You can configure the CRL with the list of certificate files you no longer trust by using the DER or PKCS#7 (P7B) formats. +You can configure the CRL to include the list of certificate files you no longer trust in DER or PKCS#7 (P7B) format. * For the DER format, pass DER-encoded CRLs. * For the PKCS#7 format, pass the `SignedData` object, where the only significant field is `crls`. @@ -646,11 +661,11 @@ quarkus.tls.certificate-revocation-list=ca.crl, ca2.crl ==== Trusting all certificates and hostname verification -You can configure your TLS connection to trust all certificates and disable the hostname verification. +You can configure your TLS connection to trust all certificates and disable hostname verification. Note that these are two different processes: -* Trusting all certificates ignores the certificate validation, so all certificates are trusted. -This method is useful for testing with self-signed certificates, but it should not be used in production. +* Trusting all certificates ignores certificate validation, so the client trusts all certificates. +This method is useful for testing with self-signed certificates, but do not use it in production. * Hostname verification is the process of verifying the server's identity. @@ -733,9 +748,9 @@ You can also use the `TlsConfiguration` object to configure the Vert.x client or == Registering a certificate from an extension -This section is only for extension developers. +The following section is only for extension developers. An extension can register a certificate in the TLS registry. -This is useful when an extension needs to provide a certificate to the application or provides a different format. +This is useful when an extension provides a certificate to the application or provides the certificate in a different format. To register a certificate in the TLS registry by using the extension, the _processor_ extension must produce a `TlsCertificateBuildItem` composed of a name and a `CertificateSupplier`. @@ -838,7 +853,7 @@ public void onCertificateUpdate(@Observes CertificateUpdatedEvent reload) { === Periodic reloading -The TLS registry includes a built-in mechanism for periodically checking the file system for changes and reloading certificates. +The TLS registry includes a built-in mechanism that periodically checks the file system for changes and reloads certificates. The `reload-period` property specifies the interval for reloading certificates and emits a `CertificateUpdatedEvent` each time certificates are reloaded. . To configure periodic certificate reloading: @@ -866,7 +881,7 @@ This is automatically handled for the Quarkus HTTP, REST, gRPC, and WebSocket se On the client side, Quarkus REST Client automatically handles certificate update events. ==== -NOTE: In Quarkus dev mode, when files are touched, it will trigger the `CertificateUpdatedEvent` much more frequently. +NOTE: In Quarkus dev mode, when files are touched, the `CertificateUpdatedEvent` is triggered much more frequently. ifndef::no-kubernetes-secrets-or-cert-manager[] == Using Kubernetes secrets or cert-manager @@ -933,7 +948,7 @@ spec: volumes: - name: my-volume secret: - defaultMode: 0666 # Set the permissions, otherwise the pod may not be able to read the files + defaultMode: 0666 # Set the permissions; otherwise, the pod may not be able to read the files optional: false secretName: my-certs # Reference the secret ---- @@ -986,7 +1001,7 @@ endif::no-kubernetes-secrets-or-cert-manager[] == Working with OpenShift serving certificates When running your application in OpenShift, you can use the link:https://docs.openshift.com/container-platform/4.16/security/certificates/service-serving-certificate.html[OpenShift serving certificates] to generate and renew TLS certificates automatically. -The Quarkus TLS registry can use these certificates and Certificate Authority (CA) files to handle HTTPS traffic and validate certificates securely. +The Quarkus TLS registry can use these certificates and Certificate Authority (CA) files to handle HTTPS traffic and securely validate certificates. [[acquiring-a-certificate]] === Acquiring a certificate @@ -1020,7 +1035,7 @@ spec: type: ClusterIP ---- -. To generate a certificate, add his annotation to your already created OpenShift `service`: +. To generate a certificate, add this annotation to your existing OpenShift `service`: + [source,shell] ---- @@ -1081,8 +1096,7 @@ spec: ---- <1> Define a volume to mount the secret. Use the same name as the secret declared above. -<2> Set up the keystore with the paths to the certificate and private key. -This can be configured by using environment variables or configuration files. +<2> By using environment variables or configuration files, set up the keystore with the paths to the certificate and private key. This example uses environment variables. OpenShift serving certificates always create the `tls.crt` and `tls.key` files. <3> Mount the secret in the container. @@ -1163,8 +1177,9 @@ spec: configMap: name: client-tls-config ---- -<1> Mount the ConfigMap in the container. -Ensure that the path matches the one used in the configuration (in this example `/deployments/tls`). +<1> Mount the ConfigMap volume in the container. +Ensure that the mount path matches the path referenced in the configuration. +In this example, `/deployments/tls`. <2> Define a volume to mount the ConfigMap and reference the ConfigMap that receives the CA certificate. . Configure the REST client to use this CA certificate. @@ -1194,7 +1209,7 @@ public interface HeroClient { <1> Configure the base URI and the configuration key. The name must be in the format `..svc`. Otherwise, the certificate will not be trusted. -Ensure that the `configKey` is also configured. +Ensure that the `configKey` is configured as well. . Configure the REST client to trust the CA certificate generated by OpenShift: + @@ -1245,33 +1260,34 @@ Commands: In most cases, you generate the Quarkus Development CA once and then generate certificates signed by this CA. The Quarkus Development CA is a Certificate Authority that can be used to sign certificates locally. -It is only valid for development purposes and only trusted on the local machine. +It is valid only for development purposes and is trusted only on the local machine. + The generated CA is located in `$HOME/.quarkus/quarkus-dev-root-ca.pem`, and installed in the system truststore. === Understanding self-signed versus CA-signed certificates When developing with TLS, you can use two types of certificates: -* **Self-signed certificate**: The certificate is signed by the same entity that uses it. -It is not trusted by default. -This type of certificate is typically used when a Certificate Authority (CA) is unavailable or when a simple setup is needed. +* **Self-signed certificate**: The owner signs the certificate with their own key. +Clients do not trust this certificate by default. +Use this certificate when a Certificate Authority (CA) is not available or when you need a simple setup. It is not suitable for production and is intended only for development. -* **CA-signed certificate**: The certificate is signed by a Certificate CA, a trusted entity. -This certificate is trusted by default and is the standard choice for production environments. +* **CA-signed certificate**: CA, a trusted entity, signs the certificate. +Clients trust this certificate by default, and it is the standard choice for production environments. While you can use a self-signed certificate for local development, it has limitations. -Browsers and tools like `curl`, `wget`, and `httpie` typically do not trust self-signed certificates, requiring manual import of the CA in your operating system. +Browsers and tools like `curl`, `wget`, and `httpie` typically do not trust self-signed certificates, so you need to import the CA into your operating system manually. To avoid this issue, use a development CA to sign certificates and install the CA in the system truststore. This ensures that the system trusts all certificates signed by the CA. -Quarkus simplifies the generation of a development CA and the certificates that are signed by this CA. +Quarkus simplifies the generation of a development CA and the certificates it signs. [[generate-a-development-ca]] === Generate a development CA The development CA is a Certificate Authority that can be used to sign certificates locally. -Note that the generated CA is only valid for development purposes and can only be trusted on the local machine. +Note that the generated CA is valid only for development purposes and can be trusted only on the local machine. To generate a development CA: [source,shell] @@ -1290,7 +1306,7 @@ When this option is used, the private key is changed, so you need to regenerate If the CA expires, it will automatically renew without requiring the `--renew` option. <3> `--truststore` also generates a PKCS12 truststore containing the CA certificate. -WARNING: When installing the certificate, your system might ask for your password to install the certificate in the system truststore or ask for confirmation in a dialog on Windows. +WARNING: When installing the certificate, your system might prompt for your password to add it to the system truststore, or display a confirmation dialog on Windows. IMPORTANT: On Windows, run as administrator from an elevated terminal to install the CA in the system truststore. @@ -1327,14 +1343,14 @@ Generate a TLS certificate with the Quarkus Dev CA if available. -d, --directory= The directory in which the certificates will be created. Default is `.certs` - -n, --name= Name of the certificate. It will be used as file name and + -n, --name= Name of the certificate. It will be used as a file name and alias in the keystore -p, --password= The password of the keystore. Default is 'password' -r, --renew Whether existing certificates will need to be replaced ---- + -A `.env` file is also generated when generating the certificate, making the Quarkus dev mode aware of these certificates. +The command also generates a `.env` file alongside the certificate so Quarkus dev mode can detect these certificates. . Run your application in dev mode to use these certificates: + @@ -1368,7 +1384,7 @@ This generates a self-signed certificate that the Quarkus Development CA does no === Uninstalling the Quarkus Development CA -Uninstalling the Quarkus Development CA from your system depends on your OS. +Uninstalling the Quarkus Development CA depends on your OS. ==== Deleting the CA certificate on Windows @@ -1433,13 +1449,13 @@ ifndef::no-lets-encrypt[] link:https://letsencrypt.org[Let's Encrypt] is a free, automated certificate authority provided by link:https://www.abetterinternet.org/[Internet Security Research Group]. -Let's Encrypt uses link:https://datatracker.ietf.org/doc/html/rfc8555[Automated certificate management environment (ACME) protocol] to support automatic certificate issuance and renewal. +Let's Encrypt uses the link:https://datatracker.ietf.org/doc/html/rfc8555[Automated Certificate Management Environment (ACME)] protocol to support automatic certificate issuance and renewal. To learn more about Let's Encrypt and ACME, see link:https://letsencrypt.org/docs/[Let's Encrypt documentation]. The TLS registry extension allows a CLI ACME client to issue and renew Let's Encrypt certificates. Your application uses this TLS registry extension to resolve ACME protocol challenges. -Follow the steps below to have your Quarkus application prepared and automatically updated with new and renewed Let's Encrypt certificates. +Follow the steps below to prepare your Quarkus application and automatically update it with new and renewed Let's Encrypt certificates. [[lets-encrypt-prerequisites]] === Prerequisites @@ -1456,7 +1472,7 @@ quarkus.tls.lets-encrypt.enabled=true ---- + The TLS registry can manage the challenge process from either the main HTTP interface or the management interface. -Using a management interface is **strongly** recommended to let Quarkus deal with ACME challenge configuration separately from the main application's deployment and security requirements: +Using a management interface is **strongly** recommended to let Quarkus deal with the ACME challenge configuration separately from the main application's deployment and security requirements: + [source,properties] ---- @@ -1480,10 +1496,9 @@ quarkus.tls.lets-encrypt.enabled=true quarkus.management.enabled=true quarkus.http.insecure-requests=redirect ---- - ==== -The challenge is served from the primary HTTP interface, which is accessible from your DNS domain name. +The primary HTTP interface serves the challenge and is accessible from your DNS domain name. IMPORTANT: Do not start your application yet. @@ -1555,15 +1570,15 @@ Use `https://localhost:8443/` if you choose not to enable a management router in + [NOTE] ==== -When the Let's Encrypt certificate chain and private key have been successfully acquired, they are converted to PEM format and copied to your application's `.letsencrypt` folder. -The TLS registry is informed that a new certificate and private key are ready and reloads them automatically. +After the `issue-certificate` command acquires the Let's Encrypt certificate chain and private key, the TLS registry CLI converts them to PEM format and copies them to the `.letsencrypt` folder. +The TLS registry detects the new certificate and private key and reloads them automatically. ==== + -. Access your application's endpoint using `https://your-domain-name:8443/` again. -Confirm in the browser that the Let's Encrypt certificate authority is now signing your domain certificate. +. Access your application's endpoint again at `https://your-domain-name:8443/`. +Confirm that the browser shows Let's Encrypt as the certificate issuer. + -Note that currently, the `issue-certificate` command implicitly creates a Let's Encrypt account to make it easy for users to get started with the ACME protocol. -Support for the Let's Encrypt account management will evolve further. +The `issue-certificate` command currently creates a Let's Encrypt account automatically to simplify initial ACME setup. +The TLS registry CLI continues to add more support for Let's Encrypt account management. === Renewing a certificate @@ -1577,23 +1592,24 @@ quarkus tls lets-encrypt renew-certificate \ --domain= ---- -During this command, TLS registry CLI reads a Let's Encrypt account information recorded during the <> step, issues a Let's Encrypt certificate request, and communicates with a Quarkus application to have ACME challenges resolved. +During this command, the TLS registry CLI reads the Let's Encrypt account information recorded during the <> step, issues a Let's Encrypt certificate request, and communicates with the Quarkus application to resolve ACME challenges. -Once the Let's Encrypt certificate chain and private key have been successfully renewed, they are converted to PEM format and copied to your application's `.letsencrypt` folder. -The TLS registry is notified when a new certificate and private key are ready, and it automatically reloads them. +After the `renew-certificate` command renews the Let's Encrypt certificate chain and private key, the TLS registry CLI converts them to PEM format and copies them to the `.letsencrypt` folder. +The TLS registry detects the new certificate and private key and reloads them automatically. [[lets-encrypt-ngrok]] === Testing with ngrok -link:https://ngrok.com/[ngrok] can be used to provide a secure HTTPS tunnel to your application running on localhost, and make it easy to test HTTPS based applications. +link:https://ngrok.com/[ngrok] provides a secure HTTPS tunnel to an application running on localhost and helps you test HTTPS-based applications. -ngrok provides a simplified way of getting started with the Quarkus Let's Encrypt ACME feature. +ngrok provides a simple way to get started with the Quarkus Let's Encrypt ACME feature. -. Initiate testing by asking ngrok to reserve a domain: +. Start testing by reserving a domain in ngrok: + -You can use link:https://github.com/quarkiverse/quarkus-ngrok[Quarkiverse ngrok] in dev mode or reserve it directly in the ngrok dashboard. -Unfortunately, you cannot use your ngrok domain to test the Quarkus Let's Encrypt ACME feature immediately. -This is because ngrok itself uses Let's Encrypt and intercepts ACME challenges that are meant to be handled by the Quarkus application instead. +You can reserve the domain by using link:https://github.com/quarkiverse/quarkus-ngrok[Quarkiverse ngrok] in dev mode or by reserving it directly in the ngrok dashboard. +You cannot use the ngrok domain to test the Quarkus Let's Encrypt ACME feature immediately. ++ +ngrok uses Let's Encrypt and intercepts ACME challenges that the Quarkus application would otherwise handle. . Therefore, remove the ngrok Let's Encrypt certificate policy from your ngrok domain: + @@ -1602,9 +1618,9 @@ This is because ngrok itself uses Let's Encrypt and intercepts ACME challenges t ngrok api --api-key reserved-domains delete-certificate-management-policy ---- + -`YOUR-RESERVED-DOMAIN-ID` is your reserved domain's id which starts from `rd_`, you can find it in the link:https://dashboard.ngrok.com/cloud-edge/domains[ngrok dashboard domains section]. +`YOUR-RESERVED-DOMAIN-ID` is your reserved domain's ID, which starts from `rd_`; you can find it in the link:https://dashboard.ngrok.com/cloud-edge/domains[ngrok dashboard domains section]. -. Because ngrok only forwards ACME challenges over HTTP, start ngrok by using the following command: +. Because ngrok only forwards ACME challenges over HTTP, start ngrok with the following command: + [source,shell] ---- @@ -1616,4 +1632,4 @@ Note that the application will be accessible from `http://YOUR-NGROK-DOMAIN` on . Test the Quarkus Let's Encrypt ACME feature from your local machine. endif::no-lets-encrypt[] // The reason for this ifndef condition is that this content is not supported in product docs. -// Feel free to add more content to this chapter, but make sure this condition encloses it. +// Feel free to add more content to this chapter, but make sure this condition encloses it. \ No newline at end of file diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index a3efbd6256f73..9bf9b90e543af 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -229,11 +229,12 @@ public BuildAotOptimizedContainerImageResultBuildItem buildAotOptimizedContainer OutputTargetBuildItem outputTargetBuildItem, DockerConfig dockerConfig, ContainerImageConfig containerImageConfig, + ContainerImageInfoBuildItem containerImageInfo, BuildAotOptimizedContainerImageRequestBuildItem requestBuildItem) { // TODO: this needs a lot of hardening as for the time being it assumes the image is in the docker daemon and only writes the new one there String baseImage = requestBuildItem.getOriginalContainerImage(); - String enhancedImage = requestBuildItem.getOriginalContainerImage() + "-aot"; + String enhancedImage = requestBuildItem.getOriginalContainerImage() + containerImageConfig.effectiveAotImageSuffix(); Path outputDirectory = outputTargetBuildItem.getOutputDirectory(); @@ -256,6 +257,8 @@ public BuildAotOptimizedContainerImageResultBuildItem buildAotOptimizedContainer throw new UnsupportedOperationException("Unable to save enhanced Dockerfile contents to disk", e); } + boolean pushContainerImage = containerImageConfig.isPushExplicitlyEnabled(); + String executableName = getExecutableName(dockerConfig, ContainerRuntime.DOCKER, ContainerRuntime.PODMAN); var dockerBuildArgs = getDockerBuildArgs(enhancedImage, new DockerfilePaths() { @Override @@ -268,7 +271,12 @@ public Path dockerExecutionPath() { return outputDirectory; } }, containerImageConfig, - dockerConfig, false, executableName, Collections.emptyList()); + dockerConfig, pushContainerImage, executableName, Collections.emptyList()); + + boolean useBuildx = dockerConfig.buildx().useBuildx(); + if (useBuildx && pushContainerImage) { + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, executableName); + } LOG.infof("Executing the following command to build image: '%s %s'", executableName, String.join(" ", dockerBuildArgs)); @@ -278,6 +286,11 @@ public Path dockerExecutionPath() { .error().logOnSuccess(false).inherited() .run(); + if (!useBuildx && pushContainerImage) { + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, executableName); + pushImage(enhancedImage, executableName, dockerConfig); + } + LOG.infof("Created AOT enhanced container image %s", enhancedImage); return new BuildAotOptimizedContainerImageResultBuildItem(enhancedImage); } diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 401146ab19f00..5a745ceb81aae 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -141,7 +141,8 @@ public void jvmStartupOptimizerArchive(ContainerImageConfig containerImageConfig } producer.produce( - new JvmStartupOptimizerArchiveContainerImageBuildItem(determineBaseJvmImage(jibConfig, compiledJavaVersion))); + new JvmStartupOptimizerArchiveContainerImageBuildItem(determineBaseJvmImage(jibConfig, compiledJavaVersion), + jibConfig.jvmAdditionalArguments())); } private String determineBaseJvmImage(ContainerImageJibConfig jibConfig, CompiledJavaVersionBuildItem compiledJavaVersion) { @@ -944,14 +945,27 @@ public boolean test(Path path) { @BuildStep public BuildAotOptimizedContainerImageResultBuildItem buildAotOptimizedContainerImageBuildItem( ContainerImageJibConfig jibConfig, + ContainerImageConfig containerImageConfig, BuildAotOptimizedContainerImageRequestBuildItem requestBuildItem) { // TODO: this needs a lot of hardening as for the time being it assumes the image is in the docker daemon and only writes the new one there String baseImage = requestBuildItem.getOriginalContainerImage(); - String enhancedImage = requestBuildItem.getOriginalContainerImage() + "-aot"; + String enhancedImage = requestBuildItem.getOriginalContainerImage() + containerImageConfig.effectiveAotImageSuffix(); + + boolean pushContainerImage = containerImageConfig.isPushExplicitlyEnabled(); try { + ImageReference enhancedImageReference = ImageReference.parse(enhancedImage); + Containerizer containerizer; + if (pushContainerImage) { + RegistryImage registryImage = toRegistryImage(enhancedImageReference, containerImageConfig.username(), + containerImageConfig.password()); + containerizer = Containerizer.to(registryImage); + } else { + containerizer = dockerDaemonContainerizer(jibConfig, enhancedImageReference); + } + createPatchedInstance(jibConfig, baseImage) .addLayer( // Add the app.aot file to the working directory @@ -959,7 +973,7 @@ public BuildAotOptimizedContainerImageResultBuildItem buildAotOptimizedContainer AbsoluteUnixPath.get(requestBuildItem.getContainerWorkingDirectory())) .addEnvironmentVariable("JAVA_TOOL_OPTIONS", "-XX:AOTCache=%s".formatted(requestBuildItem.getAotFile().getFileName().toString())) - .containerize(dockerDaemonContainerizer(jibConfig, ImageReference.parse(enhancedImage))); + .containerize(containerizer); log.infof("Created AOT enhanced container image %s", enhancedImage); return new BuildAotOptimizedContainerImageResultBuildItem(enhancedImage); diff --git a/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanProcessor.java b/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanProcessor.java index 2b329994d4172..f9f82791b24f3 100644 --- a/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanProcessor.java +++ b/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanProcessor.java @@ -187,11 +187,12 @@ public BuildAotOptimizedContainerImageResultBuildItem buildAotOptimizedContainer OutputTargetBuildItem outputTargetBuildItem, PodmanConfig podmanConfig, ContainerImageConfig containerImageConfig, + ContainerImageInfoBuildItem containerImageInfo, BuildAotOptimizedContainerImageRequestBuildItem requestBuildItem) { // TODO: this needs a lot of hardening as for the time being it assumes the image is in the docker daemon and only writes the new one there String baseImage = requestBuildItem.getOriginalContainerImage(); - String enhancedImage = requestBuildItem.getOriginalContainerImage() + "-aot"; + String enhancedImage = requestBuildItem.getOriginalContainerImage() + containerImageConfig.effectiveAotImageSuffix(); Path outputDirectory = outputTargetBuildItem.getOutputDirectory(); @@ -236,6 +237,11 @@ public Path dockerExecutionPath() { .error().logOnSuccess(false).inherited() .run(); + if (containerImageConfig.isPushExplicitlyEnabled()) { + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, executableName); + pushImage(enhancedImage, executableName, podmanConfig); + } + LOG.infof("Created AOT enhanced container image %s", enhancedImage); return new BuildAotOptimizedContainerImageResultBuildItem(enhancedImage); } diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java index d24acb240acc7..dc7a8633b9fb9 100644 --- a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java +++ b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java @@ -88,6 +88,18 @@ public interface ContainerImageConfig { */ Optional builder(); + /** + * The suffix to be used to create a new container image when an AOT file has been created. + * When this value is empty, Quarkus will interpret this as a request to not create a separate + * container image, but instead will just re-create the original one with the AOT file included + */ + @WithDefault("-aot") + Optional aotImageSuffix(); + + default String effectiveAotImageSuffix() { + return aotImageSuffix().orElse("").strip(); + } + default boolean isBuildExplicitlyEnabled() { return build().isPresent() && build().get(); } diff --git a/extensions/cyclonedx/generator/src/main/java/io/quarkus/cyclonedx/generator/CycloneDxSbomGenerator.java b/extensions/cyclonedx/generator/src/main/java/io/quarkus/cyclonedx/generator/CycloneDxSbomGenerator.java index 825f5289d760f..f3fcbd446f3b8 100644 --- a/extensions/cyclonedx/generator/src/main/java/io/quarkus/cyclonedx/generator/CycloneDxSbomGenerator.java +++ b/extensions/cyclonedx/generator/src/main/java/io/quarkus/cyclonedx/generator/CycloneDxSbomGenerator.java @@ -36,6 +36,7 @@ import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; +import com.github.packageurl.PackageURLBuilder; import io.quarkus.bootstrap.app.SbomResult; import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver; @@ -179,7 +180,7 @@ private static void recordDependencies(Bom bom, ApplicationComponent component, if (!component.getDependencies().isEmpty()) { final Dependency d = new Dependency(c.getBomRef()); for (var depCoords : sortAlphabetically(component.getDependencies())) { - d.addDependency(new Dependency(getPackageURL(depCoords).toString())); + d.addDependency(new Dependency(getPurl(depCoords).toString())); } bom.addDependency(d); } @@ -198,15 +199,8 @@ private org.cyclonedx.model.Component getComponent(ApplicationComponent componen var dep = component.getResolvedDependency(); if (dep != null) { initMavenComponent(dep, c); - } else if (component.getDistributionPath() != null) { - c.setBomRef(component.getDistributionPath()); - c.setType(org.cyclonedx.model.Component.Type.FILE); - c.setName(component.getPath().getFileName().toString()); - } else if (component.getPath() != null) { - final String fileName = component.getPath().getFileName().toString(); - c.setName(fileName); - c.setBomRef(fileName); - c.setType(org.cyclonedx.model.Component.Type.FILE); + } else if (component.getDistributionPath() != null || component.getPath() != null) { + initGenericComponent(component, c); } else { throw new RuntimeException("Component is not associated with any file system path"); } @@ -247,12 +241,21 @@ private org.cyclonedx.model.Component getComponent(ApplicationComponent componen return c; } + private static void initGenericComponent(ApplicationComponent component, Component c) { + c.setName(component.getPath().getFileName().toString()); + c.setVersion(component.getVersion()); + c.setType(Component.Type.FILE); + PackageURL purl = getGenericPurl(c.getName(), component.getVersion()); + c.setPurl(purl); + c.setBomRef(purl.toString()); + } + private void initMavenComponent(ArtifactCoords coords, Component c) { addPomMetadata(coords, c); c.setGroup(coords.getGroupId()); c.setName(coords.getArtifactId()); c.setVersion(coords.getVersion()); - final PackageURL purl = getPackageURL(coords); + final PackageURL purl = getPurl(coords); c.setPurl(purl); c.setBomRef(purl.toString()); c.setType(Component.Type.LIBRARY); @@ -388,7 +391,24 @@ private static void addExternalReference(final ExternalReference.Type referenceT } } - private static PackageURL getPackageURL(ArtifactCoords dep) { + private static PackageURL getGenericPurl(String name, String version) { + Objects.requireNonNull(name, "name must not be null"); + if (version == null) { + log.warn("Component " + name + " does not have a version. Please report this issue for quarkus-cyclonedx."); + } + try { + return PackageURLBuilder.aPackageURL() + .withType(PackageURL.StandardTypes.GENERIC) + .withName(name) + .withVersion(version) + .build(); + } catch (MalformedPackageURLException e) { + throw new RuntimeException( + "Failed to create a generic PURL for component with name " + name + " and version " + version, e); + } + } + + private static PackageURL getPurl(ArtifactCoords dep) { final TreeMap qualifiers = new TreeMap<>(); qualifiers.put("type", dep.getType()); if (!dep.getClassifier().isEmpty()) { @@ -402,7 +422,7 @@ private static PackageURL getPackageURL(ArtifactCoords dep) { dep.getVersion(), qualifiers, null); } catch (MalformedPackageURLException e) { - throw new RuntimeException("Failed to generate Purl for " + dep.toCompactCoords(), e); + throw new RuntimeException("Failed to generate PURL for " + dep.toCompactCoords(), e); } return purl; } diff --git a/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java b/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java index 3413a1f318392..6e064e6e1e91c 100644 --- a/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java +++ b/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java @@ -11,7 +11,7 @@ import static io.quarkus.devservices.common.ConfigureUtil.shouldConfigureSharedServiceLabel; import static io.quarkus.devservices.deployment.IsRuntimeModuleAvailable.IO_QUARKUS_DEVSERVICES_CONFIG_BUILDER_CLASS; -import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -27,6 +27,8 @@ import java.util.concurrent.SubmissionPublisher; import java.util.function.Supplier; +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; @@ -51,6 +53,7 @@ import io.quarkus.deployment.builditem.DevServicesNetworkIdBuildItem; import io.quarkus.deployment.builditem.DevServicesRegistryBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; @@ -72,6 +75,8 @@ public class DevServicesProcessor { + private static final Logger log = Logger.getLogger(DevServicesProcessor.class); + private static final String EXEC_FORMAT = "%s exec -it %s /bin/bash"; static volatile ConsoleStateManager.ConsoleContext context; @@ -80,15 +85,47 @@ public class DevServicesProcessor { @BuildStep public DevServicesNetworkIdBuildItem networkId( - Optional devServicesLauncherConfig, + DevServicesConfig devServicesConfig, + List sharedNetworkBuildItems, Optional composeProjectBuildItem) { - String networkId = composeProjectBuildItem - .map(DevServicesComposeProjectBuildItem::getDefaultNetworkId) - .or(() -> devServicesLauncherConfig.flatMap(ignored -> getSharedNetworkId())) - .orElse(null); + Optional configuredNetwork = ConfigProvider.getConfig().getOptionalValue( + "quarkus.test.container.network", String.class); + String networkId = configuredNetwork.flatMap(this::getOrCreateNetworkId) + .or(() -> composeProjectBuildItem.map(DevServicesComposeProjectBuildItem::getDefaultNetworkId)) + .orElseGet(() -> (devServicesConfig.launchOnSharedNetwork() || !sharedNetworkBuildItems.isEmpty()) + ? getSharedNetworkId() + : null); return new DevServicesNetworkIdBuildItem(networkId); } + private Optional getOrCreateNetworkId(String name) { + var networks = DockerClientFactory.lazyClient().listNetworksCmd().exec(); + for (var network : networks) { + if (network.getName().equals(name)) { + return Optional.of(network.getId()); + } + } + // if the network doesn't exist, create it + try { + // do the cleanup in a shutdown hook because there might be more services (launched via QuarkusTestResourceLifecycleManager) connected to the network + String id = DockerClientFactory.lazyClient().createNetworkCmd().withName(name).exec().getId(); + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + try { + DockerClientFactory.lazyClient().removeNetworkCmd(id).exec(); + } catch (Exception e) { + log.errorf("Unable to delete container network '%s'", id); + } + } + })); + return Optional.of(id); + } catch (Exception e) { + log.warnf(e, "Creating container network '%s' completed unsuccessfully", name); + return Optional.empty(); + } + } + @BuildStep(onlyIf = IsDevServicesSupportedByLaunchMode.class) @Produce(ServiceStartBuildItem.class) public DevServicesCustomizerBuildItem containerCustomizer(LaunchModeBuildItem launchModeBuildItem, @@ -114,29 +151,27 @@ public DevServicesCustomizerBuildItem containerCustomizer(LaunchModeBuildItem la } /** - * Get the network id from the shared testcontainers network, without forcing the creation of the network. + * Get the network id from the shared testcontainers network, Creates the SHARED Network instance if not already created * - * @return the network id if available, empty otherwise + * @return the network id if available, null otherwise */ - private Optional getSharedNetworkId() { + private String getSharedNetworkId() { try { - Field id; + Method id; Object sharedNetwork; var tccl = Thread.currentThread().getContextClassLoader(); if (tccl.getName().contains("Deployment")) { Class networkClass = tccl.getParent().loadClass("org.testcontainers.containers.Network"); sharedNetwork = networkClass.getField("SHARED").get(null); Class networkImplClass = tccl.getParent().loadClass("org.testcontainers.containers.Network$NetworkImpl"); - id = networkImplClass.getDeclaredField("id"); + id = networkImplClass.getDeclaredMethod("getId"); } else { sharedNetwork = Network.SHARED; - id = Network.NetworkImpl.class.getDeclaredField("id"); + id = Network.NetworkImpl.class.getDeclaredMethod("getId"); } - id.setAccessible(true); - String value = (String) id.get(sharedNetwork); - return Optional.ofNullable(value); + return (String) id.invoke(sharedNetwork); } catch (Exception e) { - return Optional.empty(); + return null; } } diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java index d9a446ea86ee7..c5facd8473857 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -59,12 +59,12 @@ import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; -import io.quarkus.deployment.builditem.Startable; import io.quarkus.deployment.dev.devservices.DevServicesConfig; import io.quarkus.devservices.common.ComposeLocator; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerAddress; import io.quarkus.devservices.common.ContainerLocator; +import io.quarkus.devservices.common.StartableContainer; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.Page; import io.quarkus.runtime.LaunchMode; @@ -112,15 +112,6 @@ public class KeycloakDevServicesProcessor { private static final String JAVA_OPTS = "JAVA_OPTS"; - /** - * This is a container label we use to mark the Keycloak container we started, as opposite to the ones we discovered. - * This value must be different to the {@link KeycloakDevServicesConfig#serviceName()}, because we don't want to - * confuse the discovered and the owned container. This is important because if the configuration for the owned - * container changes (e.g. user configured additional realms, or users, or changed the realm file), we need to - * restart the container. However, we do not restart the discovered container because we do not own it. - */ - private static final String OWNED_KEYCLOAK_SERVER_LABEL_VALUE = "quarkus-dev-service-keycloak"; - /** * Label to add to shared Dev Service for Keycloak running in containers. * This allows other applications to discover the running service and use it instead of starting a new instance. @@ -171,26 +162,24 @@ void startKeycloakContainer( .containerId(containerAddress.getId()) .config(configs) .build(); - }).orElseGet(() -> { - - return DevServicesResultBuildItem.owned().feature(feature) - .serviceName(feature.getName()) - .serviceConfig(serviceConfigHashCode) - .startable( - () -> new KeycloakServer(useSharedNetwork, config, devServicesConfig, - devServicesConfigurator, - composeProjectBuildItem, imageName)) - .postStartHook(keycloakServer -> { - for (String error : keycloakServer.errors) { - // these errors would be hidden by the 'StartupLogCompressor' if the capture was not dumped - // hence, we log them here, as by now, the compressor will surely be closed - LOG.trace(error); - } - LOG.info("Dev Services for Keycloak started."); - }) - .configProvider(createLazyConfigMap(devServicesConfigurator)) - .build(); - }); + }).orElseGet(() -> DevServicesResultBuildItem.owned().feature(feature) + .serviceName(feature.getName()) + .serviceConfig(serviceConfigHashCode) + .startable(() -> { + QuarkusOidcContainer oidcContainer = createContainer(config, useSharedNetwork, + devServicesConfig.timeout(), composeProjectBuildItem, imageName, devServicesConfigurator); + return new StartableContainer<>(oidcContainer, c -> c.getHost() + ":" + c.getPort()); + }) + .postStartHook(containerWrapper -> { + for (String error : containerWrapper.getContainer().errors) { + // these errors would be hidden by the 'StartupLogCompressor' if the capture was not dumped + // hence, we log them here, as by now, the compressor will surely be closed + LOG.trace(error); + } + LOG.info("Dev Services for Keycloak started."); + }) + .configProvider(createLazyConfigMap(devServicesConfigurator)) + .build()); devServicesResultProducer.produce(devServicesResultBuildItem); // now we know that Keycloak Dev Services will start @@ -243,13 +232,13 @@ private static int safeMapHash(Map map) { return map.entrySet().stream().mapToInt(e -> Objects.hash(e.getKey(), e.getValue())).sum(); } - private static Map> createLazyConfigMap( + private static Map, String>> createLazyConfigMap( KeycloakDevServicesConfigurator devServicesConfigurator) { return devServicesConfigurator .getLazyConfigKeys() .stream() - .map(configKey -> Map.> entry(configKey, - keycloakServer -> keycloakServer.getConfigValue(configKey))) + .map(configKey -> Map., String>> entry(configKey, + wrapper -> wrapper.getContainer().getConfigValue(configKey))) .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); } @@ -398,101 +387,30 @@ private static boolean realmDoesNotExist(String realmName, WebClient client, Str } } - // Wrap the vertx instance and the container, since both need to be started and closed - private static class KeycloakServer implements Startable { - private final boolean useSharedNetwork; - private final KeycloakDevServicesConfig config; - private final DevServicesConfig devServicesConfig; - private final String imageName; - private final KeycloakDevServicesConfigurator devServicesConfigurator; - private final DevServicesComposeProjectBuildItem composeProjectBuildItem; - private KeycloakDevServicesConfigurator.ConfigPropertiesContext configPropertiesContext; - private QuarkusOidcContainer oidcContainer; - private final List errors; - - private KeycloakServer(boolean useSharedNetwork, KeycloakDevServicesConfig config, DevServicesConfig devServicesConfig, - KeycloakDevServicesConfigurator devServicesConfigurator, - DevServicesComposeProjectBuildItem composeProjectBuildItem, String imageName) { - this.useSharedNetwork = useSharedNetwork; - this.config = config; - this.devServicesConfig = devServicesConfig; - this.devServicesConfigurator = devServicesConfigurator; - this.composeProjectBuildItem = composeProjectBuildItem; - this.imageName = imageName; - this.errors = new ArrayList<>(); - } - - @Override - public void start() { - startContainer(config, - useSharedNetwork, - devServicesConfig.timeout(), - errors, composeProjectBuildItem); - } - - @Override - public String getConnectionInfo() { - return oidcContainer.getHost() + ":" + oidcContainer.getPort(); - } - - @Override - public String getContainerId() { - return oidcContainer.getContainerId(); - } - - @Override - public void close() { - if (oidcContainer != null) { - oidcContainer.stop(); - } - } - - private void startContainer(KeycloakDevServicesConfig config, - boolean useSharedNetwork, Optional timeout, List errors, - DevServicesComposeProjectBuildItem composeProjectBuildItem) { - DockerImageName dockerImageName = DockerImageName.parse(imageName).asCompatibleSubstituteFor(imageName); - - oidcContainer = new QuarkusOidcContainer(config, dockerImageName, - config.port(), - composeProjectBuildItem.getDefaultNetworkId(), - useSharedNetwork, - config.realmPath().orElse(List.of()), - resourcesMap(config, errors), - OWNED_KEYCLOAK_SERVER_LABEL_VALUE, - config.shared(), - config.javaOpts(), - config.startCommand(), - config.features(), - config.showLogs(), - config.containerMemoryLimit(), - errors); - - timeout.ifPresent(oidcContainer::withStartupTimeout); - oidcContainer.withEnv(config.containerEnv()); - oidcContainer.start(); - - configPropertiesContext = createConfigPropertiesContext(); - } + private static QuarkusOidcContainer createContainer(KeycloakDevServicesConfig config, boolean useSharedNetwork, + Optional timeout, DevServicesComposeProjectBuildItem composeProjectBuildItem, String imageName, + KeycloakDevServicesConfigurator devServicesConfigurator) { + DockerImageName dockerImageName = DockerImageName.parse(imageName).asCompatibleSubstituteFor(imageName); - private KeycloakDevServicesConfigurator.ConfigPropertiesContext createConfigPropertiesContext() { - List errors = new ArrayList<>(); - String internalBaseUrl = getBaseURL((oidcContainer.isHttps() ? "https://" : "http://"), oidcContainer.getHost(), - oidcContainer.getPort()); - String internalUrl = startURL(internalBaseUrl, oidcContainer.keycloakX); - String hostUrl = oidcContainer.useSharedNetwork - // we need to use auto-detected host and port, so it works when docker host != localhost - ? startURL("http://", oidcContainer.getSharedNetworkExternalHost(), - oidcContainer.getSharedNetworkExternalPort(), - oidcContainer.keycloakX) - : null; + var errors = new ArrayList(); + var oidcContainer = new QuarkusOidcContainer(config, dockerImageName, + config.port(), + composeProjectBuildItem.getDefaultNetworkId(), + useSharedNetwork, + config.realmPath().orElse(List.of()), + resourcesMap(config, errors), + config.serviceName(), + config.shared(), + config.javaOpts(), + config.startCommand(), + config.features(), + config.showLogs(), + config.containerMemoryLimit(), errors, devServicesConfigurator); - return createRealmsAndReturnPropertiesContext(config, internalUrl, hostUrl, - oidcContainer.realmReps, errors, devServicesConfigurator, internalBaseUrl); - } + timeout.ifPresent(oidcContainer::withStartupTimeout); + oidcContainer.withEnv(config.containerEnv()); - private String getConfigValue(String configKey) { - return devServicesConfigurator.getLazyConfigValue(configKey, configPropertiesContext); - } + return oidcContainer; } private static Map prepareConfiguration(KeycloakDevServicesConfig config, @@ -532,7 +450,7 @@ private static String getSharedContainerUrl(ContainerAddress containerAddress) { + ":" + containerAddress.getPort(); } - private static class QuarkusOidcContainer extends GenericContainer { + private static final class QuarkusOidcContainer extends GenericContainer { private final OptionalInt fixedExposedPort; private final boolean useSharedNetwork; private final List realmPaths; @@ -548,15 +466,18 @@ private static class QuarkusOidcContainer extends GenericContainer errors; + private final KeycloakDevServicesConfig config; + private final KeycloakDevServicesConfigurator devServicesConfigurator; + private KeycloakDevServicesConfigurator.ConfigPropertiesContext configPropertiesContext; - public QuarkusOidcContainer(KeycloakDevServicesConfig config, DockerImageName dockerImageName, + private QuarkusOidcContainer(KeycloakDevServicesConfig config, DockerImageName dockerImageName, OptionalInt fixedExposedPort, String defaultNetworkId, boolean useSharedNetwork, List realmPaths, Map resources, String containerLabelValue, boolean sharedContainer, Optional javaOpts, Optional startCommand, - Optional> features, boolean showLogs, MemorySize containerMemoryLimit, - List errors) { + Optional> features, boolean showLogs, MemorySize containerMemoryLimit, List errors, + KeycloakDevServicesConfigurator devServicesConfigurator) { super(dockerImageName); this.useSharedNetwork = useSharedNetwork; @@ -583,6 +504,14 @@ public QuarkusOidcContainer(KeycloakDevServicesConfig config, DockerImageName do super.setWaitStrategy(Wait.forLogMessage(".*Keycloak.*started.*", 1)); this.hostName = ConfigureUtil.configureNetwork(this, defaultNetworkId, useSharedNetwork, "keycloak"); + this.config = config; + this.devServicesConfigurator = devServicesConfigurator; + } + + @Override + public void start() { + super.start(); + configPropertiesContext = createConfigPropertiesContext(); } @Override @@ -765,6 +694,23 @@ public int getPort() { public boolean isHttps() { return startCommand.isPresent() && startCommand.get().contains("--https"); } + + private KeycloakDevServicesConfigurator.ConfigPropertiesContext createConfigPropertiesContext() { + List errors = new ArrayList<>(); + String internalBaseUrl = getBaseURL((isHttps() ? "https://" : "http://"), getHost(), getPort()); + String internalUrl = startURL(internalBaseUrl, keycloakX); + String hostUrl = useSharedNetwork + // we need to use auto-detected host and port, so it works when docker host != localhost + ? startURL("http://", getSharedNetworkExternalHost(), getSharedNetworkExternalPort(), keycloakX) + : null; + + return createRealmsAndReturnPropertiesContext(config, internalUrl, hostUrl, + realmReps, errors, devServicesConfigurator, internalBaseUrl); + } + + private String getConfigValue(String configKey) { + return devServicesConfigurator.getLazyConfigValue(configKey, configPropertiesContext); + } } private static Set getRealmFileLastModifiedDateHashCode(Optional> realms) { diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java index 9d6f5ae9f72b9..afc4561a34d4b 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java @@ -361,7 +361,7 @@ void createBuildTimeConstJsTemplate(DevUIConfig config, InternalImportMapBuildItem internalImportMapBuildItem = new InternalImportMapBuildItem(); - var mapper = DatabindCodec.mapper().writerWithDefaultPrettyPrinter(); + var mapper = DatabindCodec.mapper().writer(); Map descriptions = new HashMap<>(); Map mcpDefaultEnabled = new HashMap<>(); Map contentTypes = new HashMap<>(); diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java index 827b0c2806af4..20118b8b75eb1 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.devui.deployment; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -242,10 +243,29 @@ void registerDevUiHandlers( List content = staticContentBuildItem.getContent(); for (DevUIContent c : content) { - String parsedContent = Qute.fmt(new String(c.getTemplate()), c.getData()); Path tempFile = devUiBasePath .resolve(c.getFileName()); - Files.writeString(tempFile, parsedContent); + if (c.getData().containsKey("buildTimeData")) { + // Write build-time data directly to file to avoid building a huge String in memory + // which can cause OOM for large applications with many extensions + @SuppressWarnings("unchecked") + Map buildTimeData = (Map) c.getData().get("buildTimeData"); + try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) { + writer.write("// Generated by Quarkus during Build"); + writer.newLine(); + for (Map.Entry entry : buildTimeData.entrySet()) { + writer.write("export const "); + writer.write(entry.getKey()); + writer.write(" = "); + writer.write(entry.getValue().toString()); + writer.write(";"); + writer.newLine(); + } + } + } else { + String parsedContent = Qute.fmt(new String(c.getTemplate()), c.getData()); + Files.writeString(tempFile, parsedContent); + } urlAndPath.put(c.getFileName(), tempFile.toString()); if (c.getDescriptions() != null && !c.getDescriptions().isEmpty()) { diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ContinuousTestingProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ContinuousTestingProcessor.java index a544103bde464..d8945d6bc427e 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ContinuousTestingProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ContinuousTestingProcessor.java @@ -280,7 +280,7 @@ private void registerGetResultsMethod(LaunchModeBuildItem launchModeBuildItem, B */ private void registerGetResultsMCPMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { actions.actionBuilder() - .methodName("getContinuousTestingResults") + .methodName("getTestResults") .description("Get the results of a Continuous testing test run") .enableMcpFuctionByDefault() .function(ignored -> { diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpToolsService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpToolsService.java index 124564e3c3ac9..a2ee67b966a90 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpToolsService.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpToolsService.java @@ -10,6 +10,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.jboss.logging.Logger; + import io.quarkus.devui.runtime.comms.JsonRpcRouter; import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; import io.quarkus.devui.runtime.mcp.model.tool.Tool; @@ -24,6 +26,9 @@ @ApplicationScoped public class McpToolsService { + private static final Logger LOG = Logger.getLogger(McpToolsService.class); + private static final int MAX_TOOL_NAME_LENGTH = 50; + @Inject JsonRpcRouter jsonRpcRouter; @@ -83,6 +88,10 @@ private void addTool(List tools, JsonRpcMethod method, Filter filter) { private Tool toTool(JsonRpcMethod jsonRpcMethod) { Tool tool = new Tool(); tool.name = jsonRpcMethod.getMethodName(); + if (tool.name != null && tool.name.length() > MAX_TOOL_NAME_LENGTH) { + LOG.warnf("MCP tool name '%s' exceeds %d characters. Some MCP clients may not support long tool names.", + tool.name, MAX_TOOL_NAME_LENGTH); + } if (jsonRpcMethod.getDescription() != null && !jsonRpcMethod.getDescription().isBlank()) { tool.description = jsonRpcMethod.getDescription(); } diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientRuntimeConfig.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientRuntimeConfig.java index 006d2d62dcc3d..00eb19342ccdc 100644 --- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientRuntimeConfig.java +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientRuntimeConfig.java @@ -54,10 +54,9 @@ public interface InfinispanClientRuntimeConfig { * remote TLS host. * * `HASH_DISTRIBUTION_AWARE` - Like `TOPOLOGY_AWARE` but with the additional advantage that each request * involving keys will be routed to the server who is the primary owner which improves performance - * greatly. This is the default. + * greatly. This is the default mode in Infinispan. */ // @formatter:on - @WithDefault("HASH_DISTRIBUTION_AWARE") Optional clientIntelligence(); // @formatter:off diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanRecorder.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanRecorder.java index da44f3363769a..085cb560da41b 100644 --- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanRecorder.java +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanRecorder.java @@ -110,7 +110,11 @@ public void eagerInitAllCaches() { RemoteCache.class, Any.Literal.INSTANCE); for (InstanceHandle handle : allCaches.handles()) { - handle.get(); // Force init + // handle.get() returns the ClientProxy for ApplicationScoped beans. + // We must invoke a method on it to trigger arc$delegate() and force + // the actual bean creation (including the blocking getCache() call) + // here on the main thread, rather than lazily on the Vert.x event loop. + handle.get().getName(); } } } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/HttpRootPathDevModeTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/HttpRootPathDevModeTest.java new file mode 100644 index 0000000000000..77ee3b34bfbdc --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/HttpRootPathDevModeTest.java @@ -0,0 +1,37 @@ +package io.quarkus.resteasy.reactive.server.test.devmode; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class HttpRootPathDevModeTest { + @RegisterExtension + static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(Resource.class) + .addAsResource(new StringAsset("%dev.quarkus.http.root-path=/custom-path"), "application.properties")); + + @Test + void rootPath() { + RestAssured.given() + .get("/custom-path/test") + .then() + .statusCode(200); + } + + @Path("/test") + public static class Resource { + @GET + public String get() { + return "Hello!"; + } + } +} diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java index 125c84eddf876..74c8a46135836 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java @@ -8,6 +8,7 @@ import org.jboss.resteasy.reactive.common.util.PathHelper; import org.jboss.resteasy.reactive.server.core.Deployment; import org.jboss.resteasy.reactive.server.handlers.ClassRoutingHandler; +import org.jboss.resteasy.reactive.server.handlers.RestInitialHandler; import org.jboss.resteasy.reactive.server.mapping.RequestMapper; import io.quarkus.runtime.RuntimeValue; @@ -54,24 +55,37 @@ private boolean shouldHandle(RoutingContext event) { public static void setTemplatePath(RoutingContext rc, Deployment deployment) { // do what RestInitialHandler does var initMappers = new RequestMapper<>(deployment.getClassMappers()); - var requestMatch = initMappers.map(getPathWithoutPrefix(rc, deployment)); - if (requestMatch == null) { - return; + var path = getPathWithoutPrefix(rc, deployment); + var requestMatch = initMappers.map(path); + + // try each class-level match until we find one whose method-level mapper also matches + // this mirrors what ClassRoutingHandler does with restartWithNextInitialMatch() + while (requestMatch != null) { + var templatePath = tryMatchTemplatePath(rc, requestMatch); + if (templatePath != null) { + if (templatePath.endsWith("/")) { + templatePath = templatePath.substring(0, templatePath.length() - 1); + } + setUrlPathTemplate(rc, templatePath); + return; + } + requestMatch = initMappers.continueMatching(path, requestMatch); } + } + + private static String tryMatchTemplatePath(RoutingContext rc, + RequestMapper.RequestMatch requestMatch) { var remaining = requestMatch.remaining.isEmpty() ? "/" : requestMatch.remaining; var serverRestHandlers = requestMatch.value.handlers; if (serverRestHandlers == null || serverRestHandlers.length < 1) { - // nothing we can do - return; + return null; } var firstHandler = serverRestHandlers[0]; - if (!(firstHandler instanceof ClassRoutingHandler)) { - // nothing we can do - return; + if (!(firstHandler instanceof ClassRoutingHandler classRoutingHandler)) { + return null; } - var classRoutingHandler = (ClassRoutingHandler) firstHandler; var mappers = classRoutingHandler.getMappers(); var requestMethod = rc.request().method().name(); @@ -86,8 +100,7 @@ public static void setTemplatePath(RoutingContext rc, Deployment deployment) { mapper = mappers.get(null); } if (mapper == null) { - // can't match the path - return; + return null; } } var target = mapper.map(remaining); @@ -100,17 +113,11 @@ public static void setTemplatePath(RoutingContext rc, Deployment deployment) { } if (target == null) { - // can't match the path - return; + return null; } } - var templatePath = requestMatch.template.template + target.template.template; - if (templatePath.endsWith("/")) { - templatePath = templatePath.substring(0, templatePath.length() - 1); - } - - setUrlPathTemplate(rc, templatePath); + return requestMatch.template.template + target.template.template; } private static String getPathWithoutPrefix(RoutingContext rc, Deployment deployment) { diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationComponent.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationComponent.java index 7a53289fbfe6f..2d4c2370116aa 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationComponent.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationComponent.java @@ -26,6 +26,11 @@ public Builder(ApplicationComponent component) { super(component); } + public Builder setVersion(String version) { + this.version = version; + return this; + } + public Builder setPath(Path componentPath) { path = componentPath; return this; @@ -73,6 +78,7 @@ protected ApplicationComponent ensureImmutable() { } } + protected String version; protected Path path; protected String distributionPath; protected ResolvedDependency dep; @@ -84,6 +90,7 @@ private ApplicationComponent() { } private ApplicationComponent(ApplicationComponent builder) { + this.version = builder.version; this.path = builder.path; this.distributionPath = builder.distributionPath; this.dep = builder.dep; @@ -92,6 +99,10 @@ private ApplicationComponent(ApplicationComponent builder) { this.dependencies = List.copyOf(builder.dependencies); } + public String getVersion() { + return version == null ? (dep == null ? null : dep.getVersion()) : version; + } + public Path getPath() { return path; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifest.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifest.java index 2cdaead983b59..aad817442e711 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifest.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifest.java @@ -39,7 +39,9 @@ private static void addRemainingContent(ApplicationManifestConfig config, Applic Files.walkFileTree(config.getDistributionDirectory(), new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - builder.addComponent(ApplicationComponent.builder().setPath(file)); + builder.addComponent(ApplicationComponent.builder() + .setVersion(config.getMainComponent().getVersion()) + .setPath(file)); return FileVisitResult.CONTINUE; } }); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifestConfig.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifestConfig.java index 5560acdad228a..8cc73fe704278 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifestConfig.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/ApplicationManifestConfig.java @@ -137,6 +137,7 @@ public ApplicationManifestConfig build() { } if (holder.path != null && !holder.path.equals(holder.component.getPath())) { holder.component = ApplicationComponent.builder() + .setVersion(holder.component.getVersion()) .setPath(holder.path) .setResolvedDependency(holder.component.getResolvedDependency()) .setDependencies(holder.component.getDependencies()) diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/RecompilationDependenciesDevModeTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/RecompilationDependenciesDevModeTest.java index acf415c7926f2..93373bae6cd42 100644 --- a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/RecompilationDependenciesDevModeTest.java +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/RecompilationDependenciesDevModeTest.java @@ -75,6 +75,7 @@ void testDependencyChangeTriggersRecompilationOfRecompilationTargets() { boolean found = false; for (LogRecord logRecord : TEST.getLogRecords()) { if (logRecord.getLoggerName().equals("io.quarkus.deployment.dev.RuntimeUpdatesProcessor") + && logRecord.getParameters() != null && logRecord.getParameters().length > 0 && (logRecord.getParameters()[0].equals("AddressMapper.class, ContactData.class") || logRecord.getParameters()[0].equals("ContactData.class, AddressMapper.class"))) { found = true; diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java index 2a5d04c52b7d4..5f1c9bdd4c696 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java @@ -33,7 +33,6 @@ public void testBasicMultiModuleBuild() throws Exception { gradleConfigurationCache(false); runGradleWrapper(projectDir, "-Dquarkus.example.name=cheburashka", - "-Dquarkus.example.runtime-name=crocodile", "-Dquarkus.package.jar.type=mutable-jar", ":example-extension:example-extension-deployment:build", // this quarkusIntTest will make sure runtime config properties passed as env vars when launching the app are effective diff --git a/integration-tests/micrometer-security/src/main/java/io/quarkus/it/micrometer/security/SecuredResourceOverlapping.java b/integration-tests/micrometer-security/src/main/java/io/quarkus/it/micrometer/security/SecuredResourceOverlapping.java new file mode 100644 index 0000000000000..5efbe6805b2df --- /dev/null +++ b/integration-tests/micrometer-security/src/main/java/io/quarkus/it/micrometer/security/SecuredResourceOverlapping.java @@ -0,0 +1,20 @@ +package io.quarkus.it.micrometer.security; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.smallrye.mutiny.Uni; + +@Path("/secured/{message}") +public class SecuredResourceOverlapping { + + @GET + @Path("/details") + @Produces(MediaType.TEXT_PLAIN) + public Uni details(@PathParam("message") String message) { + return Uni.createFrom().item("details of " + message); + } +} diff --git a/integration-tests/micrometer-security/src/test/java/io/quarkus/it/micrometer/security/SecuredResourceTest.java b/integration-tests/micrometer-security/src/test/java/io/quarkus/it/micrometer/security/SecuredResourceTest.java index dfed5aab9f601..b85c13f4dfc27 100644 --- a/integration-tests/micrometer-security/src/test/java/io/quarkus/it/micrometer/security/SecuredResourceTest.java +++ b/integration-tests/micrometer-security/src/test/java/io/quarkus/it/micrometer/security/SecuredResourceTest.java @@ -29,4 +29,27 @@ void testMetricsForUnauthorizedRequest() { ); } + @Test + void testMetricsForUnauthorizedRequestWithOverlappingPaths() { + // When two controllers have overlapping paths (e.g. /secured/{message} and /secured/{message}/details), + // the URI template should still be resolved correctly for unauthorized requests. + // See https://github.com/quarkusio/quarkus/issues/53030 + when().get("/secured/foo") + .then() + .statusCode(403); + + when().get("/secured/foo/details") + .then() + .statusCode(403); + + when().get("/q/metrics") + .then() + .statusCode(200) + .body( + allOf( + not(containsString("/secured/foo")), + containsString("/secured/{message}"), + containsString("/secured/{message}/details"))); + } + } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java index 03f890d82ebfd..09b6593e975c9 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java @@ -48,6 +48,7 @@ interface DevServicesLaunchResult extends AutoCloseable { String networkId(); + @Deprecated boolean manageNetwork(); CuratedApplication getCuratedApplication(); diff --git a/test-framework/jacoco/deployment/src/main/java/io/quarkus/jacoco/deployment/JacocoProcessor.java b/test-framework/jacoco/deployment/src/main/java/io/quarkus/jacoco/deployment/JacocoProcessor.java index a2359c4e9845b..c727f74f24c7c 100644 --- a/test-framework/jacoco/deployment/src/main/java/io/quarkus/jacoco/deployment/JacocoProcessor.java +++ b/test-framework/jacoco/deployment/src/main/java/io/quarkus/jacoco/deployment/JacocoProcessor.java @@ -83,6 +83,9 @@ void transform(BuildProducer bytecodeTransformers, } else { dataFilePath = outputDir.resolve(JacocoConfig.JACOCO_QUARKUS_EXEC); } + // Make sure the parent directory of the data file exists + Files.createDirectories(dataFilePath.getParent()); + String dataFile = dataFilePath.toString(); log.debugf("JaCoCo destFile: %s", dataFilePath); diff --git a/test-framework/junit/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index a19bee62fcf11..551b819fad7b6 100644 --- a/test-framework/junit/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -1,6 +1,5 @@ package io.quarkus.test.junit; -import static io.quarkus.deployment.util.ContainerRuntimeUtil.detectContainerRuntime; import static io.quarkus.runtime.configuration.QuarkusConfigBuilderCustomizer.QUARKUS_PROFILE; import static io.quarkus.test.common.PathTestHelper.getAppClassLocationForTestLocation; import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; @@ -29,7 +28,6 @@ import jakarta.enterprise.inject.Alternative; import jakarta.inject.Inject; -import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.spi.ConfigSource; @@ -49,7 +47,6 @@ import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesNetworkIdBuildItem; import io.quarkus.deployment.builditem.DevServicesRegistryBuildItem; -import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.paths.PathList; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.logging.LoggingSetupRecorder; @@ -59,7 +56,6 @@ import io.quarkus.test.common.TestClassIndexer; import io.quarkus.test.common.TestResourceManager; import io.quarkus.test.common.http.TestHTTPResourceManager; -import io.smallrye.common.process.ProcessBuilder; import io.smallrye.config.SmallRyeConfig; public final class IntegrationTestUtil { @@ -262,7 +258,6 @@ public void accept(String configProfile) { Map propertyMap = new HashMap<>(); AugmentAction augmentAction; - String networkId = null; if (isDockerAppLaunch) { // when the application is going to be launched as a docker container, we need to make containers started by DevServices // use a shared network that the application container can then use as well @@ -281,55 +276,8 @@ public void accept(String s, String s2) { }, DevServicesLauncherConfigResultBuildItem.class.getName(), DevServicesNetworkIdBuildItem.class.getName(), DevServicesRegistryBuildItem.class.getName(), DevServicesCustomizerBuildItem.class.getName()); - networkId = propertyMap.get("quarkus.test.container.network"); - boolean manageNetwork = false; - if (isDockerAppLaunch) { - if (networkId == null) { - // use the network the use has specified or else just generate one if none is configured - Optional networkIdOpt = ConfigProvider.getConfig().getOptionalValue( - "quarkus.test.container.network", String.class); - if (networkIdOpt.isPresent()) { - networkId = networkIdOpt.get(); - } else { - networkId = "quarkus-integration-test-" + RandomStringUtils.insecure().next(5, true, false); - manageNetwork = true; - } - } - } - - DefaultDevServicesLaunchResult result = new DefaultDevServicesLaunchResult(propertyMap, networkId, manageNetwork, - curatedApplication); - createNetworkIfNecessary(result); - return result; - } - - // this probably isn't the best place for this method, but we need to create the docker container before - // user code is aware of the network - private static void createNetworkIfNecessary( - final ArtifactLauncher.InitContext.DevServicesLaunchResult devServicesLaunchResult) { - if (devServicesLaunchResult.manageNetwork() && (devServicesLaunchResult.networkId() != null)) { - ContainerRuntimeUtil.ContainerRuntime containerRuntime = detectContainerRuntime(true); - - try { - ProcessBuilder.exec(containerRuntime.getExecutableName(), "network", "create", - devServicesLaunchResult.networkId()); - // do the cleanup in a shutdown hook because there might be more services (launched via QuarkusTestResourceLifecycleManager) connected to the network - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - @Override - public void run() { - try { - ProcessBuilder.exec(containerRuntime.getExecutableName(), "network", "rm", - devServicesLaunchResult.networkId()); - } catch (Exception e) { - System.out.printf("Unable to delete container network '%s'", devServicesLaunchResult.networkId()); - } - } - })); - } catch (Exception e) { - throw new RuntimeException("Creating container network '%s' completed unsuccessfully" - .formatted(devServicesLaunchResult.networkId()), e); - } - } + String networkId = propertyMap.get("quarkus.test.container.network"); + return new DefaultDevServicesLaunchResult(propertyMap, networkId, curatedApplication); } static void activateLogging() { @@ -350,14 +298,12 @@ static void activateLogging() { static class DefaultDevServicesLaunchResult implements ArtifactLauncher.InitContext.DevServicesLaunchResult { private final Map properties; private final String networkId; - private final boolean manageNetwork; private final CuratedApplication curatedApplication; DefaultDevServicesLaunchResult(Map properties, String networkId, - boolean manageNetwork, CuratedApplication curatedApplication) { + CuratedApplication curatedApplication) { this.properties = properties; this.networkId = networkId; - this.manageNetwork = manageNetwork; this.curatedApplication = curatedApplication; } @@ -371,7 +317,7 @@ public String networkId() { @Override public boolean manageNetwork() { - return manageNetwork; + return false; } @Override