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 unambiguousTemplate;
public InjectableTemplate(String path, Map templateVariants, Engine engine) {
@@ -179,10 +183,7 @@ public String getId() {
@Override
public Fragment getFragment(String identifier) {
- if (unambiguousTemplate != null) {
- return unambiguousTemplate.get().getFragment(identifier);
- }
- throw ambiguousTemplates("getFragment()");
+ return new InjectableFragment(identifier);
}
@Override
@@ -202,6 +203,66 @@ public String toString() {
return "Injectable template [path=" + path + "]";
}
+ class InjectableFragment implements Fragment {
+
+ private final String identifier;
+
+ InjectableFragment(String identifier) {
+ this.identifier = identifier;
+ }
+
+ @Override
+ public List getExpressions() {
+ return InjectableTemplate.this.getExpressions();
+ }
+
+ @Override
+ public Expression findExpression(Predicate predicate) {
+ return InjectableTemplate.this.findExpression(predicate);
+ }
+
+ @Override
+ public String getGeneratedId() {
+ return InjectableTemplate.this.getGeneratedId();
+ }
+
+ @Override
+ public Optional getVariant() {
+ return InjectableTemplate.this.getVariant();
+ }
+
+ @Override
+ public List getParameterDeclarations() {
+ return InjectableTemplate.this.getParameterDeclarations();
+ }
+
+ @Override
+ public String getId() {
+ return identifier;
+ }
+
+ @Override
+ public Template getOriginalTemplate() {
+ return InjectableTemplate.this;
+ }
+
+ @Override
+ public Fragment getFragment(String id) {
+ return InjectableTemplate.this.getFragment(id);
+ }
+
+ @Override
+ public Set getFragmentIds() {
+ return InjectableTemplate.this.getFragmentIds();
+ }
+
+ @Override
+ public TemplateInstance instance() {
+ return new InjectableFragmentTemplateInstanceImpl(identifier);
+ }
+
+ }
+
class InjectableTemplateInstanceImpl extends TemplateInstanceBase {
InjectableTemplateInstanceImpl() {
@@ -261,7 +322,7 @@ private TemplateInstance templateInstance() {
return instance;
}
- private Template template() {
+ protected Template template() {
if (unambiguousTemplate != null) {
return unambiguousTemplate.get();
}
@@ -280,6 +341,22 @@ private Template template() {
}
}
+
+ class InjectableFragmentTemplateInstanceImpl extends InjectableTemplateInstanceImpl {
+
+ private final String identifier;
+
+ private InjectableFragmentTemplateInstanceImpl(String identifier) {
+ this.identifier = identifier;
+ }
+
+ @Override
+ protected Template template() {
+ return super.template().getFragment(identifier);
+ }
+
+ }
+
}
static class TemplateVariants {
diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/graal/ClientHttpEngineBuilder43Replacement.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/graal/ClientHttpEngineBuilder43Replacement.java
index 7fba4adb639319..050a5b54eae12c 100644
--- a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/graal/ClientHttpEngineBuilder43Replacement.java
+++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/graal/ClientHttpEngineBuilder43Replacement.java
@@ -4,6 +4,8 @@
import javax.net.ssl.SSLContext;
+import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
+import org.jboss.resteasy.client.jaxrs.ClientHttpEngineBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import com.oracle.svm.core.annotate.Alias;
@@ -16,13 +18,13 @@
* {@code SSLContext.getInstance(SSLConnectionSocketFactory.TLS)} which will fail in native when the SSL has been disabled
*/
@TargetClass(className = "org.jboss.resteasy.client.jaxrs.engines.ClientHttpEngineBuilder43")
-public final class ClientHttpEngineBuilder43Replacement {
+public final class ClientHttpEngineBuilder43Replacement implements ClientHttpEngineBuilder {
@Alias
private ResteasyClientBuilder that;
@Substitute
- public ClientHttpEngineBuilder43Replacement resteasyClientBuilder(ResteasyClientBuilder resteasyClientBuilder) {
+ public ClientHttpEngineBuilder resteasyClientBuilder(ResteasyClientBuilder resteasyClientBuilder) {
that = resteasyClientBuilder;
// make sure we only set a context if there is none or one wouldn't be created implicitly
if ((that.getSSLContext() == null) && (that.getTrustStore() == null) && (that.getKeyStore() == null)) {
@@ -34,4 +36,10 @@ public ClientHttpEngineBuilder43Replacement resteasyClientBuilder(ResteasyClient
}
return this;
}
+
+ /**
+ * Unused alias to implement the {@link ClientHttpEngineBuilder} interface
+ */
+ @Alias
+ public native ClientHttpEngine build();
}
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java
index 76d778db7fd309..3f9008fa2f998b 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java
@@ -117,12 +117,14 @@ default String render() {
}
/**
+ * If invoked upon a fragment instance then delegate to the defining template.
*
* @return an immutable list of expressions used in the template
*/
List getExpressions();
/**
+ * If invoked upon a fragment instance then delegate to the defining template.
*
* @param predicate
* @return the first expression matching the given predicate or {@code null} if no such expression is used in the template
@@ -146,12 +148,14 @@ default String render() {
String getId();
/**
+ * If invoked upon a fragment instance then delegate to the defining template.
*
* @return the template variant
*/
Optional getVariant();
/**
+ * If invoked upon a fragment instance then delegate to the defining template.
*
* @return an immutable list of all parameter declarations defined in the template
*/
diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesInitTaskWithCustomWaitForImageTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesInitTaskWithCustomWaitForImageTest.java
new file mode 100644
index 00000000000000..b1926be6897fe1
--- /dev/null
+++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesInitTaskWithCustomWaitForImageTest.java
@@ -0,0 +1,78 @@
+package io.quarkus.it.kubernetes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.quarkus.bootstrap.model.AppArtifact;
+import io.quarkus.builder.Version;
+import io.quarkus.test.ProdBuildResults;
+import io.quarkus.test.ProdModeTestResults;
+import io.quarkus.test.QuarkusProdModeTest;
+
+public class KubernetesInitTaskWithCustomWaitForImageTest {
+
+ private static final String NAME = "kubernetes-custom-wait-for";
+ private static final String WAIT_FOR_IMAGE = "my/awesome-wait-for-image";
+
+ @RegisterExtension
+ static final QuarkusProdModeTest config = new QuarkusProdModeTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class))
+ .setApplicationName(NAME)
+ .setApplicationVersion("0.1-SNAPSHOT")
+ .setLogFileName("k8s.log")
+ .overrideConfigKey("quarkus.kubernetes.init-task-defaults.wait-for-image", WAIT_FOR_IMAGE)
+ .setForcedDependencies(Arrays.asList(
+ new AppArtifact("io.quarkus", "quarkus-kubernetes", Version.getVersion()),
+ new AppArtifact("io.quarkus", "quarkus-flyway", Version.getVersion())));
+
+ @ProdBuildResults
+ private ProdModeTestResults prodModeTestResults;
+
+ @Test
+ public void assertGeneratedResources() throws IOException {
+ final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes");
+ assertThat(kubernetesDir)
+ .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json"))
+ .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml"));
+ List kubernetesList = DeserializationUtil
+ .deserializeAsList(kubernetesDir.resolve("kubernetes.yml"));
+
+ Optional deployment = kubernetesList.stream()
+ .filter(d -> "Deployment".equals(d.getKind())
+ && NAME.equals(d.getMetadata().getName()))
+ .map(d -> (Deployment) d).findAny();
+
+ assertTrue(deployment.isPresent());
+ assertThat(deployment).satisfies(j -> j.isPresent());
+ assertThat(deployment.get()).satisfies(d -> {
+ assertThat(d.getMetadata()).satisfies(m -> {
+ assertThat(m.getName()).isEqualTo(NAME);
+ });
+
+ assertThat(d.getSpec()).satisfies(deploymentSpec -> {
+ assertThat(deploymentSpec.getTemplate()).satisfies(t -> {
+ assertThat(t.getSpec()).satisfies(podSpec -> {
+ assertThat(podSpec.getInitContainers()).singleElement().satisfies(container -> {
+ assertThat(container.getName()).isEqualTo("init");
+ assertThat(container.getImage()).isEqualTo(WAIT_FOR_IMAGE);
+ });
+
+ });
+ });
+ });
+ });
+ }
+}