From f982d5ec125b0edccabd53b91ba596586f334d4a Mon Sep 17 00:00:00 2001 From: Jose Date: Fri, 26 May 2023 09:43:52 +0200 Subject: [PATCH] Add `init-tasks` property to allow the init task configuration by task name Fix https://github.com/quarkusio/quarkus/issues/33097 --- .../kubernetes/deployment/InitTaskConfig.java | 19 ++++ .../deployment/InitTaskProcessor.java | 60 +++++++------ .../deployment/KubernetesConfig.java | 16 +++- .../deployment/OpenshiftConfig.java | 16 +++- .../deployment/OpenshiftProcessor.java | 4 +- .../VanillaKubernetesProcessor.java | 4 +- ...etesWithFlywayInitWithJobDisabledTest.java | 88 +++++++++++++++++++ 7 files changed, 172 insertions(+), 35 deletions(-) create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskConfig.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitWithJobDisabledTest.java 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 new file mode 100644 index 0000000000000..f139d18963179 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskConfig.java @@ -0,0 +1,19 @@ +package io.quarkus.kubernetes.deployment; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class InitTaskConfig { + /** + * If true, the init task will be generated. Otherwise, the init task resource generation will be skipped. + */ + @ConfigItem(defaultValue = "true") + public boolean enabled; + + /** + * The init task image to use by the init-container. + */ + @ConfigItem(defaultValue = "groundnuty/k8s-wait-for:no-root-v1.7") + public String image; +} 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 64dc6308db7cc..47927799855d8 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 @@ -1,8 +1,8 @@ package io.quarkus.kubernetes.deployment; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import io.dekorate.kubernetes.config.EnvBuilder; import io.dekorate.kubernetes.decorator.AddEnvVarDecorator; @@ -26,7 +26,9 @@ public class InitTaskProcessor { static void process( String target, // kubernetes, openshift, etc. String name, - ContainerImageInfoBuildItem image, List initTasks, + ContainerImageInfoBuildItem image, + List initTasks, + Map initTasksConfig, BuildProducer jobs, BuildProducer initContainers, BuildProducer env, @@ -34,31 +36,36 @@ static void process( BuildProducer roleBindings, BuildProducer decorators) { - List initContainerWaiterArgs = new ArrayList<>(initTasks.size() + 1); - initContainerWaiterArgs.add("job"); + boolean generateRoleForJobs = false; + for (InitTaskBuildItem task : initTasks) { + InitTaskConfig config = initTasksConfig.get(task.getName()); + if (config == null || config.enabled) { + generateRoleForJobs = true; + jobs.produce(KubernetesJobBuildItem.create(image.getImage()) + .withName(task.getName()) + .withTarget(target) + .withEnvVars(task.getTaskEnvVars()) + .withCommand(task.getCommand()) + .withArguments(task.getArguments()) + .withSharedEnvironment(task.isSharedEnvironment()) + .withSharedFilesystem(task.isSharedFilesystem())); - initTasks.forEach(task -> { - initContainerWaiterArgs.add(task.getName()); - jobs.produce(KubernetesJobBuildItem.create(image.getImage()) - .withName(task.getName()) - .withTarget(target) - .withEnvVars(task.getTaskEnvVars()) - .withCommand(task.getCommand()) - .withArguments(task.getArguments()) - .withSharedEnvironment(task.isSharedEnvironment()) - .withSharedFilesystem(task.isSharedFilesystem())); + task.getAppEnvVars().forEach((k, v) -> { + decorators.produce(new DecoratorBuildItem(target, + new AddEnvVarDecorator(ApplicationContainerDecorator.ANY, name, new EnvBuilder() + .withName(k) + .withValue(v) + .build()))); + }); - task.getAppEnvVars().forEach((k, v) -> { - decorators.produce(new DecoratorBuildItem(target, - new AddEnvVarDecorator(ApplicationContainerDecorator.ANY, name, new EnvBuilder() - .withName(k) - .withValue(v) - .build()))); - - }); - }); + initContainers.produce(KubernetesInitContainerBuildItem.create(INIT_CONTAINER_WAITER_NAME, + config == null ? INIT_CONTAINER_WAITER_DEFAULT_IMAGE : config.image) + .withTarget(target) + .withArguments(List.of("job", task.getName()))); + } + } - if (!initTasks.isEmpty()) { + if (generateRoleForJobs) { roles.produce(new KubernetesRoleBuildItem("view-jobs", Collections.singletonList( new PolicyRule( Collections.singletonList("batch"), @@ -66,11 +73,6 @@ static void process( List.of("get"))), target)); roleBindings.produce(new KubernetesRoleBindingBuildItem(null, "view-jobs", false, target)); - - initContainers.produce(KubernetesInitContainerBuildItem.create(INIT_CONTAINER_WAITER_NAME, - INIT_CONTAINER_WAITER_DEFAULT_IMAGE) - .withTarget(target) - .withArguments(initContainerWaiterArgs)); } } } 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 d5fd0a1782ad7..8ece2c5d95294 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 @@ -340,11 +340,25 @@ public enum DeploymentResourceKind { * Flag to enable init task externalization. * When enabled (default), all initialization tasks * created by extensions, will be externalized as Jobs. - * In addition the deployment will wait for these jobs. + * In addition, the deployment will wait for these jobs. + * + * @Deprecated use {@link #initTasks} configuration instead */ + @Deprecated(since = "3.1", forRemoval = true) @ConfigItem(defaultValue = "true") boolean externalizeInit; + /** + * Init tasks configuration. + * + * The init tasks are automatically generated by extensions like Flyway to perform the database migration before staring + * up the application. + * + * This property is only taken into account if `quarkus.kubernetes.externalize-init` is true. + */ + @ConfigItem + Map initTasks; + /** * 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 6f731816ac42a..0b42aa55471ec 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 @@ -568,11 +568,25 @@ public EnvVarsConfig getEnv() { * Flag to enable init task externalization. * When enabled (default), all initialization tasks * created by extensions, will be externalized as Jobs. - * In addition the deployment will wait for these jobs. + * In addition, the deployment will wait for these jobs. + * + * @Deprecated use {@link #initTasks} configuration instead */ + @Deprecated(since = "3.1", forRemoval = true) @ConfigItem(defaultValue = "true") boolean externalizeInit; + /** + * Init tasks configuration. + * + * The init tasks are automatically generated by extensions like Flyway to perform the database migration before staring + * up the application. + * + * This property is only taken into account if `quarkus.openshift.externalize-init` is true. + */ + @ConfigItem + Map initTasks; + /** * 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 1f180f8d31bf1..6c02ac6a3530e 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 @@ -384,8 +384,8 @@ void externalizeInitTasks( BuildProducer decorators) { final String name = ResourceNameUtil.getResourceName(config, applicationInfo); if (config.externalizeInit) { - InitTaskProcessor.process(OPENSHIFT, name, image, initTasks, jobs, initContainers, env, roles, roleBindings, - decorators); + InitTaskProcessor.process(OPENSHIFT, name, image, initTasks, config.initTasks, + jobs, initContainers, env, roles, roleBindings, 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 ca88fe0eff3ec..8ec60a41667f7 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 @@ -293,8 +293,8 @@ void externalizeInitTasks( BuildProducer decorators) { final String name = ResourceNameUtil.getResourceName(config, applicationInfo); if (config.externalizeInit) { - InitTaskProcessor.process(KUBERNETES, name, image, initTasks, jobs, initContainers, env, roles, roleBindings, - decorators); + InitTaskProcessor.process(KUBERNETES, name, image, initTasks, config.initTasks, + jobs, initContainers, env, roles, roleBindings, decorators); } } } \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitWithJobDisabledTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitWithJobDisabledTest.java new file mode 100644 index 0000000000000..61f8134fd8b88 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitWithJobDisabledTest.java @@ -0,0 +1,88 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.fabric8.kubernetes.api.model.batch.v1.Job; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; +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 KubernetesWithFlywayInitWithJobDisabledTest { + + private static final String NAME = "kubernetes-with-flyway-with-job-disabled"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName(NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .overrideConfigKey("quarkus.kubernetes.init-tasks.\"" + NAME + "-flyway-init\".enabled", "false") + .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()).noneSatisfy(container -> { + assertThat(container.getName()).isEqualTo("init"); + }); + }); + }); + }); + }); + + Optional job = kubernetesList.stream() + .filter(j -> "Job".equals(j.getKind()) && (NAME + "-flyway-init").equals(j.getMetadata().getName())) + .map(j -> (Job) j) + .findAny(); + assertFalse(job.isPresent()); + + Optional roleBinding = kubernetesList.stream().filter( + r -> r instanceof RoleBinding && (NAME + "-view-jobs").equals(r.getMetadata().getName())) + .map(r -> (RoleBinding) r).findFirst(); + assertFalse(roleBinding.isPresent()); + } +}