diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Shutdown.java b/core/runtime/src/main/java/io/quarkus/runtime/Shutdown.java new file mode 100644 index 00000000000000..38aaf9cc3cb27c --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/Shutdown.java @@ -0,0 +1,58 @@ +package io.quarkus.runtime; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.ObserverMethod; + +/** + * This annotation is used to mark a business method of a CDI bean that should be executed during application shutdown. The + * annotated method must be non-private and non-static and declare no arguments. + *

+ * The behavior is similar to a declaration of a {@link ShutdownEvent} observer. In fact, a synthetic observer of the + * {@link ShutdownEvent} is generated for each occurence of this annotation. Within the observer, the contextual instance of a + * bean is obtained first, and then the method is invoked. + *

+ * Furthermore, {@link #value()} can be used to specify the priority of the generated observer method and thus affects observers + * ordering. + *

+ * The contextual instance is destroyed immediately after the method is invoked for {@link Dependent} beans. + *

+ * The following examples are functionally equivalent. + * + *

+ * @ApplicationScoped
+ * class Bean1 {
+ *     void onShutdown(@Observes ShutdownEvent event) {
+ *         // place the logic here
+ *     }
+ * }
+ *
+ * @ApplicationScoped
+ * class Bean2 {
+ *
+ *     @Shutdown
+ *     void shutdown() {
+ *         // place the logic here
+ *     }
+ * }
+ * 
+ * + * @see ShutdownEvent + */ +@Target(METHOD) +@Retention(RUNTIME) +public @interface Shutdown { + + /** + * + * @return the priority + * @see jakarta.annotation.Priority + */ + int value() default ObserverMethod.DEFAULT_PRIORITY; + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Startup.java b/core/runtime/src/main/java/io/quarkus/runtime/Startup.java index 58bf079f15cd70..e7f79520f62ac6 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/Startup.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/Startup.java @@ -27,7 +27,7 @@ * Furthermore, {@link #value()} can be used to specify the priority of the generated observer method and thus affects observers * ordering. *

- * The contextual instance is destroyed immediately afterwards for {@link Dependent} beans. + * The contextual instance is destroyed immediately after the method is invoked for {@link Dependent} beans. *

* The following examples are functionally equivalent. * diff --git a/core/runtime/src/main/java/io/quarkus/runtime/graal/AwtImageIO.java b/core/runtime/src/main/java/io/quarkus/runtime/graal/AwtImageIO.java index c16d2c4a5d12af..14bc91f9f75975 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/graal/AwtImageIO.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/graal/AwtImageIO.java @@ -1,6 +1,6 @@ package io.quarkus.runtime.graal; -import java.awt.Graphics; +import java.awt.GraphicsEnvironment; import java.awt.Toolkit; import java.util.Iterator; import java.util.function.BooleanSupplier; @@ -41,7 +41,7 @@ public boolean getAsBoolean() { @TargetClass(className = "java.awt.GraphicsEnvironment", onlyWith = AwtImageIO.IsAWTAbsent.class) final class Target_java_awt_GraphicsEnvironment { @Substitute - public static Graphics getLocalGraphicsEnvironment() { + public static GraphicsEnvironment getLocalGraphicsEnvironment() { throw new UnsupportedOperationException(AwtImageIO.AWT_EXTENSION_HINT); } diff --git a/docs/src/main/asciidoc/flyway.adoc b/docs/src/main/asciidoc/flyway.adoc index 27ce233f27c3a0..c622c76cf69e10 100644 --- a/docs/src/main/asciidoc/flyway.adoc +++ b/docs/src/main/asciidoc/flyway.adoc @@ -290,18 +290,37 @@ once and then start the actual application without Flyway. To support this use c the generated manifests contain a Kubernetes initialization `Job` for Flyway. The `Job` performs initialization and the actual `Pod`, will starts once the `Job` is successfully completed. +=== Disabling + The feature is enabled by default and can be globally disabled, using: [source,properties] ---- -quarkus.kubernetes.externalize-init=false +quarkus.kubernetes.init-task-defaults.enabled=false ---- or on OpenShift: [source,properties] ---- -quarkus.openshift.externalize-init=false +quarkus.openshift.init-task-defaults.enabled=false +---- + +=== Using a custom image that controls waiting for the Job + +To change the `wait-for` image which by default is `groundnuty/k8s-wait-for:no-root-v1.7` you can use: + +[source,properties] +---- +quarkus.kubernetes.init-task-defaults.wait-for-image=my/wait-for-image:1.0 +---- + +or on Openshift: + + +[source,properties] +---- +quarkus.openshift.init-task-defaults.wait-for-image=my/wait-for-image:1.0 ---- **Note**: In this context globally means `for all extensions that support init task externalization`. diff --git a/docs/src/main/asciidoc/liquibase-mongodb.adoc b/docs/src/main/asciidoc/liquibase-mongodb.adoc index 9e924842583697..ac1162559ac352 100644 --- a/docs/src/main/asciidoc/liquibase-mongodb.adoc +++ b/docs/src/main/asciidoc/liquibase-mongodb.adoc @@ -159,18 +159,37 @@ once and then start the actual application without Liquibase. To support this us the generated manifests contain a Kubernetes initialization `Job` for Liquibase. The `Job` performs initialization and the actual `Pod`, will starts once the `Job` is successfully completed. +=== Disabling + The feature is enabled by default and can be globally disabled, using: [source,properties] ---- -quarkus.kubernetes.externalize-init=false +quarkus.kubernetes.init-task-defaults.enabled=false ---- or on OpenShift: [source,properties] ---- -quarkus.openshift.externalize-init=false +quarkus.openshift.init-task-defaults.enabled=false +---- + +=== Using a custom image that controls waiting for the Job + +To change the `wait-for` image which by default is `groundnuty/k8s-wait-for:no-root-v1.7` you can use: + +[source,properties] +---- +quarkus.kubernetes.init-task-defaults.wait-for-image=my/wait-for-image:1.0 +---- + +or on Openshift: + + +[source,properties] +---- +quarkus.openshift.init-task-defaults.wait-for-image=my/wait-for-image:1.0 ---- **Note**: In this context globally means `for all extensions that support init task externalization`. diff --git a/docs/src/main/asciidoc/liquibase.adoc b/docs/src/main/asciidoc/liquibase.adoc index c3249f9acd782a..428d5a23fd6ea4 100644 --- a/docs/src/main/asciidoc/liquibase.adoc +++ b/docs/src/main/asciidoc/liquibase.adoc @@ -237,20 +237,41 @@ once and then start the actual application without Liquibase. To support this us the generated manifests contain a Kubernetes initialization `Job` for Liquibase. The `Job` performs initialization and the actual `Pod`, will starts once the `Job` is successfully completed. +=== Disabling + The feature is enabled by default and can be globally disabled, using: [source,properties] ---- -quarkus.kubernetes.externalize-init=false +quarkus.kubernetes.init-task-defaults.enabled=false ---- or on OpenShift: [source,properties] ---- -quarkus.openshift.externalize-init=false +quarkus.openshift.init-task-defaults.enabled=false +---- + +=== Using a custom image that controls waiting for the Job + +To change the `wait-for` image which by default is `groundnuty/k8s-wait-for:no-root-v1.7` you can use: + +[source,properties] +---- +quarkus.kubernetes.init-task-defaults.wait-for-image=my/wait-for-image:1.0 ---- +or on Openshift: + + +[source,properties] +---- +quarkus.openshift.init-task-defaults.wait-for-image=my/wait-for-image:1.0 +---- + + + **Note**: In this context globally means `for all extensions that support init task externalization`. == Configuration Reference diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ShutdownBuildSteps.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ShutdownBuildSteps.java new file mode 100644 index 00000000000000..46980b7889a7c4 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ShutdownBuildSteps.java @@ -0,0 +1,141 @@ +package io.quarkus.arc.deployment; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.inject.spi.ObserverMethod; + +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; +import org.jboss.logging.Logger; + +import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem; +import io.quarkus.arc.impl.CreationalContextImpl; +import io.quarkus.arc.processor.AnnotationStore; +import io.quarkus.arc.processor.BeanInfo; +import io.quarkus.arc.processor.BuildExtension; +import io.quarkus.arc.processor.BuiltinScope; +import io.quarkus.arc.processor.ObserverConfigurator; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.gizmo.CatchBlockCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.gizmo.TryBlock; +import io.quarkus.runtime.Shutdown; +import io.quarkus.runtime.ShutdownEvent; + +public class ShutdownBuildSteps { + + static final DotName SHUTDOWN_NAME = DotName.createSimple(Shutdown.class.getName()); + + private static final Logger LOG = Logger.getLogger(ShutdownBuildSteps.class); + + @BuildStep + AutoAddScopeBuildItem addScope(CustomScopeAnnotationsBuildItem customScopes) { + // Class with no built-in scope annotation but with @Shutdown annotation + return AutoAddScopeBuildItem.builder() + .defaultScope(BuiltinScope.APPLICATION) + .anyMethodMatches(new Predicate() { + @Override + public boolean test(MethodInfo m) { + return m.hasAnnotation(SHUTDOWN_NAME); + } + }) + .reason("Found classes containing @Shutdown annotation.") + .build(); + } + + @BuildStep + UnremovableBeanBuildItem unremovableBeans() { + return new UnremovableBeanBuildItem(new Predicate() { + @Override + public boolean test(BeanInfo bean) { + if (bean.isClassBean()) { + return bean.getTarget().get().asClass().annotationsMap().containsKey(SHUTDOWN_NAME); + } + return false; + } + }); + } + + @BuildStep + void registerShutdownObservers(ObserverRegistrationPhaseBuildItem observerRegistration, + BuildProducer configurators) { + + AnnotationStore annotationStore = observerRegistration.getContext().get(BuildExtension.Key.ANNOTATION_STORE); + + for (BeanInfo bean : observerRegistration.getContext().beans().classBeans()) { + ClassInfo beanClass = bean.getTarget().get().asClass(); + List shutdownMethods = new ArrayList<>(); + // Collect all non-static no-args methods annotated with @Shutdown + for (MethodInfo method : beanClass.methods()) { + if (annotationStore.hasAnnotation(method, SHUTDOWN_NAME)) { + if (!method.isSynthetic() + && !Modifier.isPrivate(method.flags()) + && !Modifier.isStatic(method.flags()) + && method.parametersCount() == 0) { + shutdownMethods.add(method); + } else { + LOG.warnf("Ignored an invalid @Shutdown method declared on %s: %s", method.declaringClass().name(), + method); + } + } + } + if (!shutdownMethods.isEmpty()) { + for (MethodInfo method : shutdownMethods) { + AnnotationValue priority = annotationStore.getAnnotation(method, SHUTDOWN_NAME).value(); + registerShutdownObserver(observerRegistration, bean, + method.declaringClass().name() + "#" + method.toString(), + priority != null ? priority.asInt() : ObserverMethod.DEFAULT_PRIORITY, method); + } + } + } + } + + private void registerShutdownObserver(ObserverRegistrationPhaseBuildItem observerRegistration, BeanInfo bean, String id, + int priority, MethodInfo shutdownMethod) { + ObserverConfigurator configurator = observerRegistration.getContext().configure() + .beanClass(bean.getBeanClass()) + .observedType(ShutdownEvent.class); + configurator.id(id); + configurator.priority(priority); + configurator.notify(mc -> { + // InjectableBean bean = Arc.container().bean("bflmpsvz"); + ResultHandle containerHandle = mc.invokeStaticMethod(StartupBuildSteps.ARC_CONTAINER); + ResultHandle beanHandle = mc.invokeInterfaceMethod(StartupBuildSteps.ARC_CONTAINER_BEAN, containerHandle, + mc.load(bean.getIdentifier())); + if (BuiltinScope.DEPENDENT.is(bean.getScope())) { + ResultHandle creationalContext = mc.newInstance( + MethodDescriptor.ofConstructor(CreationalContextImpl.class, Contextual.class), + beanHandle); + // Create a dependent instance + ResultHandle instance = mc.invokeInterfaceMethod(StartupBuildSteps.CONTEXTUAL_CREATE, beanHandle, + creationalContext); + TryBlock tryBlock = mc.tryBlock(); + tryBlock.invokeVirtualMethod(MethodDescriptor.of(shutdownMethod), instance); + CatchBlockCreator catchBlock = tryBlock.addCatch(Exception.class); + catchBlock.invokeInterfaceMethod(StartupBuildSteps.CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext); + catchBlock.throwException(RuntimeException.class, "Error destroying bean with @Shutdown method", + catchBlock.getCaughtException()); + // Destroy the instance immediately + mc.invokeInterfaceMethod(StartupBuildSteps.CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext); + } else { + // Obtains the instance from the context + // InstanceHandle handle = Arc.container().instance(bean); + ResultHandle instanceHandle = mc.invokeInterfaceMethod(StartupBuildSteps.ARC_CONTAINER_INSTANCE, + containerHandle, + beanHandle); + ResultHandle instance = mc.invokeInterfaceMethod(StartupBuildSteps.INSTANCE_HANDLE_GET, instanceHandle); + mc.invokeVirtualMethod(MethodDescriptor.of(shutdownMethod), instance); + } + mc.returnValue(null); + }); + configurator.done(); + } +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java index b95bd0f1bf49fb..477eee7c6d506c 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java @@ -21,6 +21,7 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; +import org.jboss.logging.Logger; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; @@ -39,8 +40,10 @@ import io.quarkus.arc.processor.ObserverConfigurator; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.gizmo.TryBlock; import io.quarkus.runtime.Startup; import io.quarkus.runtime.StartupEvent; @@ -61,6 +64,8 @@ public class StartupBuildSteps { static final MethodDescriptor CONTEXTUAL_DESTROY = MethodDescriptor.ofMethod(Contextual.class, "destroy", void.class, Object.class, CreationalContext.class); + private static final Logger LOG = Logger.getLogger(StartupBuildSteps.class); + @BuildStep AutoAddScopeBuildItem addScope(CustomScopeAnnotationsBuildItem customScopes) { // Class with no built-in scope annotation but with @Startup annotation @@ -136,12 +141,17 @@ void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistr // If the target is a class then collect all non-static non-producer no-args methods annotated with @Startup startupMethods = new ArrayList<>(); for (MethodInfo method : target.asClass().methods()) { - if (!method.isSynthetic() - && !Modifier.isStatic(method.flags()) - && method.parametersCount() == 0 - && annotationStore.hasAnnotation(method, STARTUP_NAME) - && !annotationStore.hasAnnotation(method, DotNames.PRODUCES)) { - startupMethods.add(method); + if (annotationStore.hasAnnotation(method, STARTUP_NAME)) { + if (!method.isSynthetic() + && !Modifier.isPrivate(method.flags()) + && !Modifier.isStatic(method.flags()) + && method.parametersCount() == 0 + && !annotationStore.hasAnnotation(method, DotNames.PRODUCES)) { + startupMethods.add(method); + } else { + LOG.warnf("Ignored an invalid @Startup method declared on %s: %s", method.declaringClass().name(), + method); + } } } } @@ -177,9 +187,14 @@ private void registerStartupObserver(ObserverRegistrationPhaseBuildItem observer ResultHandle instance = mc.invokeInterfaceMethod(CONTEXTUAL_CREATE, beanHandle, creationalContext); if (startupMethod != null) { - mc.invokeVirtualMethod(MethodDescriptor.of(startupMethod), instance); + TryBlock tryBlock = mc.tryBlock(); + tryBlock.invokeVirtualMethod(MethodDescriptor.of(startupMethod), instance); + CatchBlockCreator catchBlock = tryBlock.addCatch(Exception.class); + catchBlock.invokeInterfaceMethod(CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext); + catchBlock.throwException(RuntimeException.class, "Error destroying bean with @Startup method", + catchBlock.getCaughtException()); } - // But destroy the instance immediately + // Destroy the instance immediately mc.invokeInterfaceMethod(CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext); } else { // Obtains the instance from the context diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/Messages.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/Messages.java new file mode 100644 index 00000000000000..83fb75c34ec56f --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/Messages.java @@ -0,0 +1,9 @@ +package io.quarkus.arc.test.shutdown; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class Messages { + + public static final List MESSAGES = new CopyOnWriteArrayList<>(); +} \ No newline at end of file diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationInvalidMethodTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationInvalidMethodTest.java new file mode 100644 index 00000000000000..4ea9ea9859ee5a --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationInvalidMethodTest.java @@ -0,0 +1,37 @@ +package io.quarkus.arc.test.shutdown; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.Shutdown; +import io.quarkus.test.QuarkusUnitTest; + +public class ShutdownAnnotationInvalidMethodTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(ShutdownMethods.class)) + .setLogRecordPredicate(r -> r.getLoggerName().contains("ShutdownBuildSteps")) + .assertLogRecords(list -> { + assertEquals(1, list.size()); + assertTrue(list.get(0).getMessage().startsWith("Ignored an invalid @Shutdown method declared on")); + }); + + @Test + public void test() { + } + + // @ApplicationScoped is added automatically + static class ShutdownMethods { + + @Shutdown + void invalid(String name) { + } + + } + +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationTest.java new file mode 100644 index 00000000000000..411796f31f42b7 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationTest.java @@ -0,0 +1,52 @@ +package io.quarkus.arc.test.shutdown; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.annotation.PostConstruct; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.Shutdown; +import io.quarkus.test.QuarkusUnitTest; + +public class ShutdownAnnotationTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(ShutdownMethods.class)) + .setAllowTestClassOutsideDeployment(true) + .setAfterUndeployListener(() -> { + assertEquals(3, Messages.MESSAGES.size()); + assertEquals("shutdown_pc", Messages.MESSAGES.get(0)); + assertEquals("shutdown_first", Messages.MESSAGES.get(1)); + assertEquals("shutdown_second", Messages.MESSAGES.get(2)); + }); + + @Test + public void test() { + } + + // @ApplicationScoped is added automatically + static class ShutdownMethods { + + @Shutdown + String first() { + Messages.MESSAGES.add("shutdown_first"); + return "ok"; + } + + @Shutdown(Integer.MAX_VALUE) + void second() { + Messages.MESSAGES.add("shutdown_second"); + } + + @PostConstruct + void init() { + Messages.MESSAGES.add("shutdown_pc"); + } + + } + +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskConfig.java index f139d189631799..e1a295726258ad 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskConfig.java @@ -1,5 +1,7 @@ package io.quarkus.kubernetes.deployment; +import java.util.Optional; + import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -11,9 +13,17 @@ public class InitTaskConfig { @ConfigItem(defaultValue = "true") public boolean enabled; + /** + * The init task image to use by the init-container. + */ + @Deprecated + @ConfigItem + public Optional image; + /** * The init task image to use by the init-container. */ @ConfigItem(defaultValue = "groundnuty/k8s-wait-for:no-root-v1.7") - public String image; + public String waitForImage; + } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java index 58fb10d804e160..3d7be4b9fe4ba2 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java @@ -22,13 +22,13 @@ public class InitTaskProcessor { private static final String INIT_CONTAINER_WAITER_NAME = "init"; - private static final String INIT_CONTAINER_WAITER_DEFAULT_IMAGE = "groundnuty/k8s-wait-for:no-root-v1.7"; static void process( String target, // kubernetes, openshift, etc. String name, ContainerImageInfoBuildItem image, List initTasks, + InitTaskConfig initTaskDefaults, Map initTasksConfig, BuildProducer jobs, BuildProducer initContainers, @@ -40,7 +40,7 @@ static void process( boolean generateRoleForJobs = false; for (InitTaskBuildItem task : initTasks) { - InitTaskConfig config = initTasksConfig.get(task.getName()); + InitTaskConfig config = initTasksConfig.getOrDefault(task.getName(), initTaskDefaults); if (config == null || config.enabled) { generateRoleForJobs = true; jobs.produce(KubernetesJobBuildItem.create(image.getImage()) @@ -60,8 +60,8 @@ static void process( .build()))); }); - initContainers.produce(KubernetesInitContainerBuildItem.create(INIT_CONTAINER_WAITER_NAME, - config == null ? INIT_CONTAINER_WAITER_DEFAULT_IMAGE : config.image) + String waitForImage = config.image.orElse(config.waitForImage); + initContainers.produce(KubernetesInitContainerBuildItem.create(INIT_CONTAINER_WAITER_NAME, waitForImage) .withTarget(target) .withArguments(List.of("job", task.getName()))); } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java index cd31abd1d669fc..999d9c6b0f5a1a 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java @@ -381,6 +381,15 @@ public enum DeploymentResourceKind { @ConfigItem Map initTasks; + /** + * Default Init tasks configuration. + * + * The init tasks are automatically generated by extensions like Flyway to perform the database migration before staring + * up the application. + */ + @ConfigItem + InitTaskConfig initTaskDefaults; + /** * Switch used to control whether non-idempotent fields are included in generated kubernetes resources to improve * git-ops compatibility diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java index 3c3062eb7f3eb8..a4ed700237fcfe 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java @@ -598,6 +598,15 @@ public EnvVarsConfig getEnv() { @ConfigItem Map initTasks; + /** + * Default Init tasks configuration. + * + * The init tasks are automatically generated by extensions like Flyway to perform the database migration before staring + * up the application. + */ + @ConfigItem + InitTaskConfig initTaskDefaults; + /** * Switch used to control whether non-idempotent fields are included in generated kubernetes resources to improve * git-ops compatibility diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java index 7c6f38b8697fc6..6e8747568b32a4 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java @@ -406,7 +406,7 @@ void externalizeInitTasks( BuildProducer decorators) { final String name = ResourceNameUtil.getResourceName(config, applicationInfo); if (config.externalizeInit) { - InitTaskProcessor.process(OPENSHIFT, name, image, initTasks, config.initTasks, + InitTaskProcessor.process(OPENSHIFT, name, image, initTasks, config.initTaskDefaults, config.initTasks, jobs, initContainers, env, roles, roleBindings, serviceAccount, decorators); } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java index 1e1e904a25acdf..04d6f0c5d1859d 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java @@ -319,7 +319,7 @@ void externalizeInitTasks( BuildProducer decorators) { final String name = ResourceNameUtil.getResourceName(config, applicationInfo); if (config.externalizeInit) { - InitTaskProcessor.process(KUBERNETES, name, image, initTasks, config.initTasks, + InitTaskProcessor.process(KUBERNETES, name, image, initTasks, config.initTaskDefaults, config.initTasks, jobs, initContainers, env, roles, roleBindings, serviceAccount, decorators); } } diff --git a/extensions/oidc/deployment/src/main/resources/dev-ui/qwc-oidc-provider.js b/extensions/oidc/deployment/src/main/resources/dev-ui/qwc-oidc-provider.js index 0f3ab5f10ea749..fd55fda88612ba 100644 --- a/extensions/oidc/deployment/src/main/resources/dev-ui/qwc-oidc-provider.js +++ b/extensions/oidc/deployment/src/main/resources/dev-ui/qwc-oidc-provider.js @@ -493,6 +493,7 @@ export class QwcOidcProvider extends QwcHotReloadElement { this.jsonRpc .testServiceWithPassword({ tokenUrl: this._getTokenUrl(), + serviceUrl: null, clientId: this._getClientId(), clientSecret: this._getClientSecret(), username: this._passwordGrantUsername, @@ -510,6 +511,7 @@ export class QwcOidcProvider extends QwcHotReloadElement { this.jsonRpc .testServiceWithPassword({ tokenUrl: this._getTokenUrl(), + serviceUrl: null, clientId: this._getClientId(), clientSecret: this._getClientSecret(), username: this._passwordGrantUsername, @@ -542,6 +544,7 @@ export class QwcOidcProvider extends QwcHotReloadElement { this.jsonRpc .testServiceWithClientCred({ tokenUrl: this._getTokenUrl(), + serviceUrl: null, clientId: this._getClientId(), clientSecret: this._getClientSecret() }) @@ -557,6 +560,7 @@ export class QwcOidcProvider extends QwcHotReloadElement { this.jsonRpc .testServiceWithClientCred({ tokenUrl: this._getTokenUrl(), + serviceUrl: null, clientId: this._getClientId(), clientSecret: this._getClientSecret() }) diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentTest.java index c5a7cb3502be36..4b143f2c73ff77 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentTest.java @@ -11,6 +11,7 @@ import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateGlobal; import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Variant; import io.quarkus.test.QuarkusUnitTest; public class CheckedTemplateFragmentTest { @@ -26,10 +27,21 @@ public class CheckedTemplateFragmentTest { "{#fragment id=foo}{#for i in bar}{i_count}. <{i}>{#if i_hasNext}, {/if}{/for}{/fragment}"), "templates/CheckedTemplateFragmentTest/foos.html")); + @SuppressWarnings("unchecked") @Test public void testFragment() { - assertEquals("Foo", Templates.items(null).getFragment("item").data("it", new Item("Foo")).render()); - assertEquals("Foo", Templates.items$item(new Item("Foo")).render()); + TemplateInstance items = Templates.items(null); + List variants = (List) items.getAttribute(TemplateInstance.VARIANTS); + assertEquals(1, variants.size()); + assertEquals("text/html", variants.get(0).getContentType()); + assertEquals("Foo", items.getFragment("item").data("it", new Item("Foo")).render()); + + TemplateInstance fragment = Templates.items$item(new Item("Foo")); + variants = (List) fragment.getAttribute(TemplateInstance.VARIANTS); + assertEquals(1, variants.size()); + assertEquals("text/html", variants.get(0).getContentType()); + assertEquals("Foo", fragment.render()); + assertEquals("FooAndBar is a long name", Templates.items$item(new Item("FooAndBar")).render()); assertEquals("1. <1>, 2. <2>, 3. <3>, 4. <4>, 5. <5>", Templates.foos$foo().render()); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentVariantTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentVariantTest.java new file mode 100644 index 00000000000000..62461b07bbd1dd --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentVariantTest.java @@ -0,0 +1,54 @@ +package io.quarkus.qute.deployment.typesafe.fragment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Variant; +import io.quarkus.test.QuarkusUnitTest; + +public class CheckedTemplateFragmentVariantTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Templates.class, Item.class) + .addAsResource(new StringAsset( + "{#each items}{#fragment id='item'}

{it.name}

{/fragment}{/each}"), + "templates/CheckedTemplateFragmentVariantTest/items.html") + .addAsResource(new StringAsset( + "{#each items}{#fragment id='item'}{it.name}{/fragment}{/each}"), + "templates/CheckedTemplateFragmentVariantTest/items.txt")); + + @SuppressWarnings("unchecked") + @Test + public void testFragment() { + TemplateInstance fragment = Templates.items$item(new Item("Foo")); + List variants = (List) fragment.getAttribute(TemplateInstance.VARIANTS); + assertEquals(2, variants.size()); + + assertEquals("

Foo

", + fragment.setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType("text/html")).render()); + assertEquals("Foo", + fragment.setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType("text/plain")).render()); + // A variant for application/json does not exist, use the default - html wins + assertEquals("

Foo

", + fragment.setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType("application/json")).render()); + } + + @CheckedTemplate + public static class Templates { + + static native TemplateInstance items(List items); + + static native TemplateInstance items$item(Item it); + + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java index 10b9900f0272f7..b35a7e5efc9990 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -87,9 +87,6 @@ Template getTemplate(InjectionPoint injectionPoint) { if (path == null || path.isEmpty()) { throw new IllegalStateException("No template location specified"); } - // We inject a delegating template in order to: - // 1. Be able to select an appropriate variant if needed - // 2. Be able to reload the template when needed, i.e. when the cache is cleared return new InjectableTemplate(path, templateVariants, engine); } @@ -100,11 +97,18 @@ public Template getInjectableTemplate(String path) { return new InjectableTemplate(path, templateVariants, engine); } + /** + * We inject a delegating template in order to: + * + * 1. Be able to select an appropriate variant if needed + * 2. Be able to reload the template when needed, i.e. when the cache is cleared + */ static class InjectableTemplate implements Template { private final String path; private final TemplateVariants variants; private final Engine engine; + // Some methods may only work if a single template variant is found private final LazyValue