diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java index 5af46ef4d0b2fc..0e3047769b0911 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java @@ -127,4 +127,6 @@ public interface Capability { String CONFLUENT_REGISTRY = QUARKUS_PREFIX + "confluent.registry"; String CONFLUENT_REGISTRY_AVRO = CONFLUENT_REGISTRY + ".avro"; + + String PICOCLI = QUARKUS_PREFIX + "picocli"; } diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 4128b0c53197d2..c99c87deec3313 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -147,13 +147,6 @@ The full source of the `kubernetes.json` file looks something like this: } ---- -Beside generating a `Deployment` resource, you can also choose to get a `StatefulSet` instead via `application.properties`: - -[source,properties] ----- -quarkus.kubernetes.deployment-kind=StatefulSet ----- - The generated manifest can be applied to the cluster from the project root using `kubectl`: [source,bash] @@ -175,6 +168,47 @@ quarkus.container-image.tag=1.0 #optional, defaults to the application ver The image that will be used in the generated manifests will be `quarkus/demo-app:1.0` +=== Changing the generated deployment resource + +Besides generating a `Deployment` resource, you can also choose to generate either a `StatefulSet`, or a `Job`, or a `CronJob` resource instead via `application.properties`: + +[source,properties] +---- +quarkus.kubernetes.deployment-kind=StatefulSet +---- + +==== Generating Job resources + +If you want to generate a Job resource, you need to add the following property to the `application.properties`: + +[source,properties] +---- +quarkus.kubernetes.deployment-kind=Job +---- + +IMPORTANT: If you are using the Picocli extension, by default a Job resource will be generated. + +You can provide the arguments that will be used by the Kubernetes Job via the property `quarkus.kubernetes.arguments`. For example, by adding the property `quarkus.kubernetes.arguments=A,B`. + +Finally, the Kubernetes job will be launched every time it is installed in Kubernetes. You can know more about how to run Kubernetes jobs in this https://kubernetes.io/docs/concepts/workloads/controllers/job/#running-an-example-job[link]. + +You can configure the rest of the Kubernetes Job configuration using the properties under `quarkus.kubernetes.job.xxx` (see https://quarkus.io/guides/deploying-to-kubernetes#quarkus-kubernetes-kubernetes-config_quarkus.kubernetes.job.parallelism-parallelism[link]). + +==== Generating CronJob resources + +If you want to generate a CronJob resource, you need to add the following property via the `application.properties`: + +[source,properties] +---- +quarkus.kubernetes.deployment-kind=CronJob +# Cron expression to run the job every hour +quarkus.kubernetes.cron-job.schedule=0 * * * * +---- + +IMPORTANT: CronJob resources require the https://en.wikipedia.org/wiki/Cron[Cron] expression to specify when to launch the job via the property `quarkus.kubernetes.cron-job.schedule`. If not provide, the build will fail. + +You can configure the rest of the Kubernetes CronJob configuration using the properties under `quarkus.kubernetes.cron-job.xxx` (see https://quarkus.io/guides/deploying-to-kubernetes#quarkus-kubernetes-kubernetes-config_quarkus.kubernetes.cron-job.parallelism-parallelism[link]). + === Namespace By default, Quarkus omits the namespace in the generated manifests, rather than enforce the `default` namespace. That means that you can apply the manifest to your chosen namespace when using `kubectl`, which in the example below is `test`: @@ -532,7 +566,7 @@ implementation("io.quarkus:quarkus-smallrye-health") The values of the generated probes will be determined by the configured health properties: `quarkus.smallrye-health.root-path`, `quarkus.smallrye-health.liveness-path` and `quarkus.smallrye-health.readiness-path`. More information about the health extension can be found in the relevant xref:microprofile-health.adoc[guide]. -=== Customizing the readiness probe: +=== Customizing the readiness probe To set the initial delay of the probe to 20 seconds and the period to 45: [source,properties] diff --git a/docs/src/main/asciidoc/deploying-to-openshift.adoc b/docs/src/main/asciidoc/deploying-to-openshift.adoc index ec1b0613924236..acaf50a233a09e 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift.adoc @@ -354,7 +354,16 @@ It's also possible to use the value from another field to add a new environment quarkus.openshift.env.fields.foo=metadata.name ---- -==== Using Deployment instead of DeploymentConfig +==== Changing the generated deployment resource + +Beside generating a `DeploymentConfig` resource, you can also choose to get either a `Deployment`, `StatefulSet`, or a `Job`, or a `CronJob` resource instead via `application.properties`: + +[source,properties] +---- +quarkus.openshift.deployment-kind=StatefulSet +---- + +===== Using Deployment instead of DeploymentConfig Out of the box the extension will generate a `DeploymentConfig` resource. Often users, prefer to use `Deployment` as the main deployment resource, but still make use of OpenShift specific resources like `Route`, `BuildConfig` etc. This feature is enabled by setting `quarkus.openshift.deployment-kind` to `Deployment`. @@ -371,6 +380,38 @@ When the image is built, using OpenShift builds (s2i binary and docker strategy) quarkus.container-image.group= ---- +===== Generating Job resources + +If you want to generate a Job resource, you need to add the following property via the `application.properties`: + +[source,properties] +---- +quarkus.openshift.deployment-kind=Job +---- + +IMPORTANT: If you are using the Picocli extension, by default the Job resource will be generated. + +You can provide the arguments that will be used by the Kubernetes Job via the property `quarkus.openshift.arguments`. For example, adding the property `quarkus.openshift.arguments=A,B`. + +Finally, the Kubernetes job will be launched every time that is installed in OpenShift. You can know more about how to run Kubernetes jobs in this https://kubernetes.io/docs/concepts/workloads/controllers/job/#running-an-example-job[link]. + +You can configure the rest of the Kubernetes Job configuration using the properties under `quarkus.openshift.job.xxx` (see https://quarkus.io/guides/deploying-to-openshift#quarkus-openshift-openshift-config_quarkus.openshift.job.parallelism[link]). + +===== Generating CronJob resources + +If you want to generate a CronJob resource, you need to add the following property via the `application.properties`: + +[source,properties] +---- +quarkus.openshift.deployment-kind=CronJob +# Cron expression to run the job every hour +quarkus.openshift.cron-job.schedule=0 * * * * +---- + +IMPORTANT: CronJob resources require the https://en.wikipedia.org/wiki/Cron[Cron] expression to specify when to launch the job via the property `quarkus.openshift.cron-job.schedule`. If not provide, the build will fail. + +You can configure the rest of the Kubernetes CronJob configuration using the properties under `quarkus.openshift.cron-job.xxx` (see https://quarkus.io/guides/deploying-to-openshift#quarkus-openshift-openshift-config_quarkus.openshift.cron-job.parallelism[link]). + ==== Validation A conflict between two definitions, e.g. mistakenly assigning both a value and specifying that a variable is derived from a field, will result in an error being thrown at build time so that you get the opportunity to fix the issue before you deploy your application to your cluster where it might be more difficult to diagnose the source of the issue. diff --git a/docs/src/main/asciidoc/picocli.adoc b/docs/src/main/asciidoc/picocli.adoc index ff25483cd4d7f5..2c6f8300be9ef1 100644 --- a/docs/src/main/asciidoc/picocli.adoc +++ b/docs/src/main/asciidoc/picocli.adoc @@ -18,10 +18,8 @@ IMPORTANT: If you are not familiar with the Quarkus Command Mode, consider readi Once you have your Quarkus project configured you can add the `picocli` extension to your project by running the following command in your project base directory. -[source,bash] ----- -./mvnw quarkus:add-extension -Dextensions="picocli" ----- +:add-extension-extensions: picocli +include::{includes}/devtools/extension-add.adoc[] This will add the following to your `pom.xml`: @@ -291,6 +289,79 @@ annotationProcessor 'info.picocli:picocli-codegen' In the development mode, i.e. when running `mvn quarkus:dev`, the application is executed and restarted every time the `Space bar` key is pressed. You can also pass arguments to your command line app via the `quarkus.args` system property, e.g. `mvn quarkus:dev -Dquarkus.args='--help'` and `mvn quarkus:dev -Dquarkus.args='-c -w --val 1'`. +== Kubernetes support + +Once you have your command line application, you can also generate the resources necessary to install and use this application in Kubernetes by adding the `kubernetes` extension. To install the `kubernetes` extension, run the following command in your project base directory. + +:add-extension-extensions: kubernetes +include::{includes}/devtools/extension-add.adoc[] + +This will add the following to your `pom.xml`: + +[source,xml] +---- + + io.quarkus + quarkus-kubernetes + +---- + +And, next, build the application with: + +include::{includes}/devtools/build.adoc[] + +The Kubernetes extension will detect the presence of the Picocli extension and hence generate a https://kubernetes.io/docs/concepts/workloads/controllers/job/[Job] resource instead of a https://kubernetes.io/docs/concepts/workloads/controllers/deployment/[Deployment] resource in the `target/kubernetes/` directory. + +IMPORTANT: If you don't want to generate a Job resource, you can specify the resource you want to generate using the property `quarkus.kubernetes.deployment-kind`. For example, if you want to generate a Deployment resource, use `quarkus.kubernetes.deployment-kind=Deployment`. + +Moreover, you can provide the arguments that will be used by the Kubernetes job via the property `quarkus.kubernetes.arguments`. For example, after adding the property `quarkus.kubernetes.arguments=A,B` and building your project, the following Job resource will be generated: + +[source,yaml] +---- +apiVersion: batch/v1 +kind: Job +metadata: + labels: + app.kubernetes.io/name: app + app.kubernetes.io/version: 0.1-SNAPSHOT + name: app +spec: + completionMode: NonIndexed + selector: + matchLabels: + app.kubernetes.io/name: app + app.kubernetes.io/version: 0.1-SNAPSHOT + suspend: false + template: + metadata: + labels: + app.kubernetes.io/name: app + app.kubernetes.io/version: 0.1-SNAPSHOT + spec: + containers: + - args: + - A + - B + env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: docker.io/user/app:0.1-SNAPSHOT + imagePullPolicy: Always + name: app + ports: + - containerPort: 8080 + name: http + protocol: TCP + restartPolicy: OnFailure + terminationGracePeriodSeconds: 10 + +---- + +Finally, the Kubernetes job will be launched every time it is installed in Kubernetes. You can know more about how to run Kubernetes jobs in this https://kubernetes.io/docs/concepts/workloads/controllers/job/#running-an-example-job[document]. + + == Configuration Reference include::{generated-dir}/config/quarkus-picocli.adoc[opts=optional, leveloffset=+1] diff --git a/extensions/kubernetes/openshift/deployment/src/main/java/io/quarkus/openshift/deployment/OpenshiftProcessor.java b/extensions/kubernetes/openshift/deployment/src/main/java/io/quarkus/openshift/deployment/OpenshiftProcessor.java index 515098ecc62f90..5644cc63da4ec9 100644 --- a/extensions/kubernetes/openshift/deployment/src/main/java/io/quarkus/openshift/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/openshift/deployment/src/main/java/io/quarkus/openshift/deployment/OpenshiftProcessor.java @@ -2,6 +2,7 @@ import static io.quarkus.kubernetes.deployment.Constants.OPENSHIFT; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; @@ -14,11 +15,11 @@ public class OpenshiftProcessor { @BuildStep - public void checkOpenshift(ApplicationInfoBuildItem applicationInfo, OpenshiftConfig config, + public void checkOpenshift(ApplicationInfoBuildItem applicationInfo, Capabilities capabilities, OpenshiftConfig config, BuildProducer deploymentTargets, BuildProducer resourceMeta) { - DeploymentResourceKind deploymentResourceKind = config.getDeploymentResourceKind(); + DeploymentResourceKind deploymentResourceKind = config.getDeploymentResourceKind(capabilities); deploymentTargets .produce( new KubernetesDeploymentTargetBuildItem(OPENSHIFT, deploymentResourceKind.kind, diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddCronJobResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddCronJobResourceDecorator.java new file mode 100644 index 00000000000000..a370d8269a0eae --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddCronJobResourceDecorator.java @@ -0,0 +1,119 @@ + +package io.quarkus.kubernetes.deployment; + +import static io.quarkus.kubernetes.deployment.Constants.CRONJOB; + +import java.util.HashMap; +import java.util.List; +import java.util.function.Function; + +import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesListFluent; +import io.fabric8.kubernetes.api.model.batch.v1.CronJob; +import io.fabric8.kubernetes.api.model.batch.v1.CronJobBuilder; +import io.fabric8.kubernetes.api.model.batch.v1.CronJobFluent; + +public class AddCronJobResourceDecorator extends ResourceProvidingDecorator> { + + private final String name; + private final CronJobConfig config; + + public AddCronJobResourceDecorator(String name, CronJobConfig config) { + this.name = name; + this.config = config; + if (!config.schedule.isPresent()) { + throw new IllegalArgumentException( + "When generating a CronJob resource, you need to specify a schedule CRON expression."); + } + } + + @SuppressWarnings("deprecation") + @Override + public void visit(KubernetesListFluent list) { + CronJobBuilder builder = list.getItems().stream() + .filter(this::containsCronJobResource) + .map(replaceExistingCronJobResource(list)) + .findAny() + .orElseGet(this::createCronJobResource) + .accept(CronJobBuilder.class, this::initCronJobResourceWithDefaults); + + list.addToItems(builder.build()); + } + + private boolean containsCronJobResource(HasMetadata metadata) { + return CRONJOB.equalsIgnoreCase(metadata.getKind()) && name.equals(metadata.getMetadata().getName()); + } + + private void initCronJobResourceWithDefaults(CronJobBuilder builder) { + CronJobFluent.SpecNested spec = builder.editOrNewSpec(); + + var jobTemplateSpec = spec + .editOrNewJobTemplate() + .editOrNewSpec(); + + jobTemplateSpec.editOrNewSelector() + .endSelector() + .editOrNewTemplate() + .editOrNewSpec() + .endSpec() + .endTemplate(); + + // defaults for: + // - match labels + if (jobTemplateSpec.getSelector().getMatchLabels() == null) { + jobTemplateSpec.editSelector().withMatchLabels(new HashMap<>()).endSelector(); + } + // - termination grace period seconds + if (jobTemplateSpec.getTemplate().getSpec().getTerminationGracePeriodSeconds() == null) { + jobTemplateSpec.editTemplate().editSpec().withTerminationGracePeriodSeconds(10L).endSpec().endTemplate(); + } + // - container + if (!containsContainerWithName(spec)) { + jobTemplateSpec.editTemplate().editSpec().addNewContainer().withName(name).endContainer().endSpec().endTemplate(); + } + + spec.withSuspend(config.suspend); + spec.withSchedule(config.schedule.get()); + spec.withConcurrencyPolicy(config.concurrencyPolicy.name()); + config.successfulJobsHistoryLimit.ifPresent(spec::withSuccessfulJobsHistoryLimit); + config.failedJobsHistoryLimit.ifPresent(spec::withFailedJobsHistoryLimit); + config.startingDeadlineSeconds.ifPresent(spec::withStartingDeadlineSeconds); + + jobTemplateSpec.withCompletionMode(config.completionMode.name()); + jobTemplateSpec.editTemplate().editSpec().withRestartPolicy(config.restartPolicy.name()).endSpec().endTemplate(); + config.parallelism.ifPresent(jobTemplateSpec::withParallelism); + config.completions.ifPresent(jobTemplateSpec::withCompletions); + config.backoffLimit.ifPresent(jobTemplateSpec::withBackoffLimit); + config.activeDeadlineSeconds.ifPresent(jobTemplateSpec::withActiveDeadlineSeconds); + config.ttlSecondsAfterFinished.ifPresent(jobTemplateSpec::withTtlSecondsAfterFinished); + + jobTemplateSpec.endSpec().endJobTemplate(); + spec.endSpec(); + } + + private CronJobBuilder createCronJobResource() { + return new CronJobBuilder().withNewMetadata().withName(name).endMetadata(); + } + + private Function replaceExistingCronJobResource(KubernetesListFluent list) { + return metadata -> { + list.removeFromItems(metadata); + return new CronJobBuilder((CronJob) metadata); + }; + } + + private boolean containsContainerWithName(CronJobFluent.SpecNested spec) { + var jobTemplate = spec.getJobTemplate(); + if (jobTemplate == null + || jobTemplate.getSpec() == null + || jobTemplate.getSpec().getTemplate() == null + || jobTemplate.getSpec().getTemplate().getSpec() == null) { + return false; + } + + List containers = jobTemplate.getSpec().getTemplate().getSpec().getContainers(); + return containers == null || containers.stream().anyMatch(c -> name.equals(c.getName())); + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddJobResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddJobResourceDecorator.java new file mode 100644 index 00000000000000..f1db3ef6991486 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddJobResourceDecorator.java @@ -0,0 +1,96 @@ + +package io.quarkus.kubernetes.deployment; + +import static io.quarkus.kubernetes.deployment.Constants.JOB; + +import java.util.HashMap; +import java.util.List; +import java.util.function.Function; + +import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesListFluent; +import io.fabric8.kubernetes.api.model.batch.v1.Job; +import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder; +import io.fabric8.kubernetes.api.model.batch.v1.JobFluent; + +public class AddJobResourceDecorator extends ResourceProvidingDecorator> { + + private final String name; + private final JobConfig config; + + public AddJobResourceDecorator(String name, JobConfig config) { + this.name = name; + this.config = config; + } + + @SuppressWarnings("deprecation") + @Override + public void visit(KubernetesListFluent list) { + JobBuilder builder = list.getItems().stream() + .filter(this::containsJobResource) + .map(replaceExistingJobResource(list)) + .findAny() + .orElseGet(this::createJobResource) + .accept(JobBuilder.class, this::initJobResourceWithDefaults); + + list.addToItems(builder.build()); + } + + private boolean containsJobResource(HasMetadata metadata) { + return JOB.equalsIgnoreCase(metadata.getKind()) && name.equals(metadata.getMetadata().getName()); + } + + private void initJobResourceWithDefaults(JobBuilder builder) { + JobFluent.SpecNested spec = builder.editOrNewSpec(); + + spec.editOrNewSelector() + .endSelector() + .editOrNewTemplate() + .editOrNewSpec() + .endSpec() + .endTemplate(); + + // defaults for: + // - match labels + if (spec.getSelector().getMatchLabels() == null) { + spec.editSelector().withMatchLabels(new HashMap<>()).endSelector(); + } + // - termination grace period seconds + if (spec.getTemplate().getSpec().getTerminationGracePeriodSeconds() == null) { + spec.editTemplate().editSpec().withTerminationGracePeriodSeconds(10L).endSpec().endTemplate(); + } + // - container + if (!containsContainerWithName(spec)) { + spec.editTemplate().editSpec().addNewContainer().withName(name).endContainer().endSpec().endTemplate(); + } + + spec.withSuspend(config.suspend); + spec.withCompletionMode(config.completionMode.name()); + spec.editTemplate().editSpec().withRestartPolicy(config.restartPolicy.name()).endSpec().endTemplate(); + config.parallelism.ifPresent(spec::withParallelism); + config.completions.ifPresent(spec::withCompletions); + config.backoffLimit.ifPresent(spec::withBackoffLimit); + config.activeDeadlineSeconds.ifPresent(spec::withActiveDeadlineSeconds); + config.ttlSecondsAfterFinished.ifPresent(spec::withTtlSecondsAfterFinished); + + spec.endSpec(); + } + + private JobBuilder createJobResource() { + return new JobBuilder().withNewMetadata().withName(name).endMetadata(); + } + + private Function replaceExistingJobResource(KubernetesListFluent list) { + return metadata -> { + list.removeFromItems(metadata); + return new JobBuilder((Job) metadata); + }; + } + + private boolean containsContainerWithName(JobFluent.SpecNested spec) { + List containers = spec.getTemplate().getSpec().getContainers(); + return containers == null || containers.stream().anyMatch(c -> name.equals(c.getName())); + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java index 78509c76709cd0..193643df5c3ad8 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java @@ -7,9 +7,13 @@ public final class Constants { public static final String KIND = "kind"; public static final String STATEFULSET = "StatefulSet"; public static final String DEPLOYMENT = "Deployment"; + public static final String JOB = "Job"; + public static final String CRONJOB = "CronJob"; public static final String DEPLOYMENT_GROUP = "apps"; public static final String DEPLOYMENT_VERSION = "v1"; public static final String INGRESS = "Ingress"; + public static final String BATCH_GROUP = "batch"; + public static final String BATCH_VERSION = "v1"; static final String DOCKER = "docker"; diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/CronJobConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/CronJobConfig.java new file mode 100644 index 00000000000000..c0fb59679e2154 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/CronJobConfig.java @@ -0,0 +1,119 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Optional; + +import io.dekorate.kubernetes.annotation.CronJobConcurrencyPolicy; +import io.dekorate.kubernetes.annotation.JobCompletionMode; +import io.dekorate.kubernetes.annotation.JobRestartPolicy; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class CronJobConfig { + + /** + * The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + * + * @return The schedule cron expression. + */ + @ConfigItem + Optional schedule; + + /** + * ConcurrencyPolicy describes how the job will be handled. + * + * @return the concurrency policy mode. + */ + @ConfigItem(defaultValue = "Allow") + CronJobConcurrencyPolicy concurrencyPolicy; + + /** + * Deadline in seconds for starting the job if it misses scheduled time for any reason. + * Missed jobs executions will be counted as failed ones. + * + * @return the starting deadline seconds attribute. + */ + @ConfigItem + Optional startingDeadlineSeconds; + + /** + * The number of failed finished jobs to retain. The default value is 1. + * + * @return the failed jobs history limit attribute. + */ + Optional failedJobsHistoryLimit; + + /** + * The number of successful finished jobs to retain. The default value is 3. + * + * @return the successful jobs history limit attribute. + */ + Optional successfulJobsHistoryLimit; + + /** + * Specifies the maximum desired number of pods the job should run at any given time. + * + * @return The desired number of pods. + */ + @ConfigItem + Optional parallelism; + + /** + * Specifies the desired number of successfully finished pods the job should be run with. + * + * @return The desired number of successfully finished pods. + */ + @ConfigItem + Optional completions; + + /** + * CompletionMode specifies how Pod completions are tracked. + * + * @return the completion mode. + */ + @ConfigItem(defaultValue = "NonIndexed") + JobCompletionMode completionMode; + + /** + * Specifies the number of retries before marking this job failed. + * + * @return The back-off limit. + */ + @ConfigItem + Optional backoffLimit; + + /** + * Specifies the duration in seconds relative to the startTime that the job may be continuously active before the system + * tries to terminate it; value must be positive integer. + * + * @return the active deadline seconds. + */ + @ConfigItem + Optional activeDeadlineSeconds; + + /** + * Limits the lifetime of a Job that has finished execution (either Complete or Failed). If this + * field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. + * + * @return the time to live seconds after finished. + */ + @ConfigItem + Optional ttlSecondsAfterFinished; + + /** + * Suspend specifies whether the Job controller should create Pods or not. + * + * @return the suspend job attribute. + */ + @ConfigItem(defaultValue = "false") + boolean suspend; + + /** + * Restart policy when the job container fails. + * + * @return the restart policy. + */ + @ConfigItem(defaultValue = "OnFailure") + JobRestartPolicy restartPolicy; + +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/JobConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/JobConfig.java new file mode 100644 index 00000000000000..15f1322804e745 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/JobConfig.java @@ -0,0 +1,79 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Optional; + +import io.dekorate.kubernetes.annotation.JobCompletionMode; +import io.dekorate.kubernetes.annotation.JobRestartPolicy; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class JobConfig { + + /** + * Specifies the maximum desired number of pods the job should run at any given time. + * + * @return The desired number of pods. + */ + @ConfigItem + Optional parallelism; + + /** + * Specifies the desired number of successfully finished pods the job should be run with. + * + * @return The desired number of successfully finished pods. + */ + @ConfigItem + Optional completions; + + /** + * CompletionMode specifies how Pod completions are tracked. + * + * @return the completion mode. + */ + @ConfigItem(defaultValue = "NonIndexed") + JobCompletionMode completionMode; + + /** + * Specifies the number of retries before marking this job failed. + * + * @return The back-off limit. + */ + @ConfigItem + Optional backoffLimit; + + /** + * Specifies the duration in seconds relative to the startTime that the job may be continuously active before the system + * tries to terminate it; value must be positive integer. + * + * @return the active deadline seconds. + */ + @ConfigItem + Optional activeDeadlineSeconds; + + /** + * Limits the lifetime of a Job that has finished execution (either Complete or Failed). If this + * field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. + * + * @return the time to live seconds after finished. + */ + @ConfigItem + Optional ttlSecondsAfterFinished; + + /** + * Suspend specifies whether the Job controller should create Pods or not. + * + * @return the suspend job attribute. + */ + @ConfigItem(defaultValue = "false") + boolean suspend; + + /** + * Restart policy when the job container fails. + * + * @return the restart policy. + */ + @ConfigItem(defaultValue = "OnFailure") + JobRestartPolicy restartPolicy; + +} 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 d2d59cca38738f..b5813a7931bf8b 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 @@ -1,6 +1,8 @@ package io.quarkus.kubernetes.deployment; +import static io.quarkus.kubernetes.deployment.Constants.CRONJOB; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT; +import static io.quarkus.kubernetes.deployment.Constants.JOB; import static io.quarkus.kubernetes.deployment.Constants.STATEFULSET; import java.util.List; @@ -10,6 +12,8 @@ import io.dekorate.kubernetes.annotation.ImagePullPolicy; import io.dekorate.kubernetes.annotation.ServiceType; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -18,7 +22,9 @@ public class KubernetesConfig implements PlatformConfiguration { public enum DeploymentResourceKind { Deployment(DEPLOYMENT), - StatefulSet(STATEFULSET); + StatefulSet(STATEFULSET), + Job(JOB), + CronJob(CRONJOB); final String kind; @@ -45,13 +51,12 @@ public enum DeploymentResourceKind { */ @ConfigItem(defaultValue = "${quarkus.container-image.tag}") Optional version; - /** * The kind of the deployment resource to use. - * Supported values are 'Deployment' and 'StatefulSet' defaulting to the first. + * Supported values are 'StatefulSet', 'Job', 'CronJob' and 'Deployment' defaulting to the latter. */ - @ConfigItem(defaultValue = "Deployment") - KubernetesConfig.DeploymentResourceKind deploymentKind; + @ConfigItem + Optional deploymentKind; /** * The namespace the generated resources should belong to. @@ -266,6 +271,16 @@ public enum DeploymentResourceKind { */ IngressConfig ingress; + /** + * Job configuration. It's only used if and only if {@code quarkus.kubernetes.deployment-kind} is `Job`. + */ + JobConfig job; + + /** + * CronJob configuration. It's only used if and only if {@code quarkus.kubernetes.deployment-kind} is `CronJob`. + */ + CronJobConfig cronJob; + /** * If true, the 'app.kubernetes.io/version' label will be part of the selectors of Service and Deployment */ @@ -320,10 +335,6 @@ public Optional getVersion() { return version; } - public String getDeploymentResourceKind() { - return deploymentKind.kind; - } - public Optional getNamespace() { return namespace; } @@ -506,4 +517,14 @@ public Optional getAppConfigMap() { public SecurityContextConfig getSecurityContext() { return securityContext; } + + public KubernetesConfig.DeploymentResourceKind getDeploymentResourceKind(Capabilities capabilities) { + if (deploymentKind.isPresent()) { + return deploymentKind.get(); + } else if (capabilities.isPresent(Capability.PICOCLI)) { + return KubernetesConfig.DeploymentResourceKind.Job; + } + + return DeploymentResourceKind.Deployment; + } } 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 549163563f006c..5d5cd3eb1a2814 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 @@ -1,12 +1,16 @@ package io.quarkus.kubernetes.deployment; +import static io.quarkus.kubernetes.deployment.Constants.BATCH_GROUP; +import static io.quarkus.kubernetes.deployment.Constants.BATCH_VERSION; +import static io.quarkus.kubernetes.deployment.Constants.CRONJOB; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_CONFIG; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_CONFIG_GROUP; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_CONFIG_VERSION; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_GROUP; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_VERSION; +import static io.quarkus.kubernetes.deployment.Constants.JOB; import static io.quarkus.kubernetes.deployment.Constants.OPENSHIFT; import static io.quarkus.kubernetes.deployment.Constants.S2I; import static io.quarkus.kubernetes.deployment.Constants.STATEFULSET; @@ -21,6 +25,7 @@ import io.quarkus.container.image.deployment.ContainerImageCapabilitiesUtil; import io.quarkus.container.image.deployment.ContainerImageConfig; import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -35,7 +40,9 @@ public static enum OpenshiftFlavor { public static enum DeploymentResourceKind { Deployment(DEPLOYMENT, DEPLOYMENT_GROUP, DEPLOYMENT_VERSION), DeploymentConfig(DEPLOYMENT_CONFIG, DEPLOYMENT_CONFIG_GROUP, DEPLOYMENT_CONFIG_VERSION), - StatefulSet(STATEFULSET, DEPLOYMENT_GROUP, DEPLOYMENT_VERSION); + StatefulSet(STATEFULSET, DEPLOYMENT_GROUP, DEPLOYMENT_VERSION), + Job(JOB, BATCH_GROUP, BATCH_VERSION), + CronJob(CRONJOB, BATCH_GROUP, BATCH_VERSION); public final String kind; public final String apiGroup; @@ -58,7 +65,7 @@ public static enum DeploymentResourceKind { /** * The kind of the deployment resource to use. - * Supported values are 'Deployment' and 'DeploymentConfig' defaulting to the latter. + * Supported values are 'Deployment', 'StatefulSet', 'Job', 'CronJob' and 'DeploymentConfig' defaulting to the latter. */ @ConfigItem Optional deploymentKind; @@ -308,6 +315,16 @@ public static enum DeploymentResourceKind { @ConfigItem(defaultValue = "true") boolean addVersionToLabelSelectors; + /** + * Job configuration. It's only used if and only if {@code quarkus.openshift.deployment-kind} is `Job`. + */ + JobConfig job; + + /** + * CronJob configuration. It's only used if and only if {@code quarkus.openshift.deployment-kind} is `CronJob`. + */ + CronJobConfig cronJob; + public Optional getPartOf() { return partOf; } @@ -538,7 +555,13 @@ public static boolean isOpenshiftBuildEnabled(ContainerImageConfig containerImag return containerImageConfig.builder.map(b -> b.equals(OPENSHIFT) || b.equals(S2I)).orElse(implicitlyEnabled); } - public DeploymentResourceKind getDeploymentResourceKind() { - return deploymentKind.orElse(DeploymentResourceKind.DeploymentConfig); + public DeploymentResourceKind getDeploymentResourceKind(Capabilities capabilities) { + if (deploymentKind.isPresent()) { + return deploymentKind.get(); + } else if (capabilities.isPresent(Capability.PICOCLI)) { + return DeploymentResourceKind.Job; + } + + return DeploymentResourceKind.DeploymentConfig; } } 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 c97cee401e0265..a16ee821453e3a 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 @@ -73,13 +73,13 @@ public class OpenshiftProcessor { private static final String OPENSHIFT_V3_APP = "app"; @BuildStep - public void checkOpenshift(ApplicationInfoBuildItem applicationInfo, OpenshiftConfig config, + public void checkOpenshift(ApplicationInfoBuildItem applicationInfo, Capabilities capabilities, OpenshiftConfig config, BuildProducer deploymentTargets, BuildProducer resourceMeta) { List targets = KubernetesConfigUtil.getUserSpecifiedDeploymentTargets(); boolean openshiftEnabled = targets.contains(OPENSHIFT); - DeploymentResourceKind deploymentResourceKind = config.getDeploymentResourceKind(); + DeploymentResourceKind deploymentResourceKind = config.getDeploymentResourceKind(capabilities); deploymentTargets.produce( new KubernetesDeploymentTargetBuildItem(OPENSHIFT, deploymentResourceKind.kind, deploymentResourceKind.apiGroup, deploymentResourceKind.apiVersion, OPENSHIFT_PRIORITY, openshiftEnabled)); @@ -96,7 +96,7 @@ public void populateInternalRegistry(OpenshiftConfig openshiftConfig, ContainerI BuildProducer containerImageRegistry) { if (!containerImageConfig.registry.isPresent()) { - DeploymentResourceKind deploymentResourceKind = openshiftConfig.getDeploymentResourceKind(); + DeploymentResourceKind deploymentResourceKind = openshiftConfig.getDeploymentResourceKind(capabilities); if (deploymentResourceKind != DeploymentResourceKind.DeploymentConfig) { if (openshiftConfig.isOpenshiftBuildEnabled(containerImageConfig, capabilities)) { // Images stored in internal openshift registry use the following pattern: @@ -215,8 +215,8 @@ public List createDecorators(ApplicationInfoBuildItem applic result.add(new DecoratorBuildItem(new RemoveOptionalFromConfigMapKeySelectorDecorator())); } - DeploymentResourceKind deploymentResourceKind = config.getDeploymentResourceKind(); - switch (deploymentResourceKind) { + DeploymentResourceKind deploymentKind = config.getDeploymentResourceKind(capabilities); + switch (deploymentKind) { case Deployment: result.add(new DecoratorBuildItem(OPENSHIFT, new RemoveDeploymentConfigResourceDecorator(name))); result.add(new DecoratorBuildItem(OPENSHIFT, new AddDeploymentResourceDecorator(name, config))); @@ -225,6 +225,14 @@ public List createDecorators(ApplicationInfoBuildItem applic result.add(new DecoratorBuildItem(OPENSHIFT, new RemoveDeploymentConfigResourceDecorator(name))); result.add(new DecoratorBuildItem(OPENSHIFT, new AddStatefulSetResourceDecorator(name, config))); break; + case Job: + result.add(new DecoratorBuildItem(OPENSHIFT, new RemoveDeploymentConfigResourceDecorator(name))); + result.add(new DecoratorBuildItem(OPENSHIFT, new AddJobResourceDecorator(name, config.job))); + break; + case CronJob: + result.add(new DecoratorBuildItem(OPENSHIFT, new RemoveDeploymentConfigResourceDecorator(name))); + result.add(new DecoratorBuildItem(OPENSHIFT, new AddCronJobResourceDecorator(name, config.cronJob))); + break; } if (config.route != null) { @@ -306,7 +314,7 @@ public List createDecorators(ApplicationInfoBuildItem applic result.add(new DecoratorBuildItem(OPENSHIFT, new ApplyHttpGetActionPortDecorator(name, name, port))); // Handle non-openshift builds - if (deploymentResourceKind == DeploymentResourceKind.DeploymentConfig + if (deploymentKind == DeploymentResourceKind.DeploymentConfig && !OpenshiftConfig.isOpenshiftBuildEnabled(containerImageConfig, capabilities)) { image.ifPresent(i -> { String registry = containerImageConfig.registry 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 f198107de2b2d1..29ddccdc69c06b 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 @@ -33,6 +33,7 @@ import io.dekorate.utils.Labels; import io.quarkus.container.spi.ContainerImageInfoBuildItem; import io.quarkus.container.spi.ContainerImageLabelBuildItem; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; @@ -57,10 +58,11 @@ public class VanillaKubernetesProcessor { @BuildStep - public void checkVanillaKubernetes(ApplicationInfoBuildItem applicationInfo, KubernetesConfig config, + public void checkVanillaKubernetes(ApplicationInfoBuildItem applicationInfo, Capabilities capabilities, + KubernetesConfig config, BuildProducer deploymentTargets, BuildProducer resourceMeta) { - String kind = config.getDeploymentResourceKind(); + String kind = config.getDeploymentResourceKind(capabilities).kind; List userSpecifiedDeploymentTargets = KubernetesConfigUtil.getUserSpecifiedDeploymentTargets(); if (userSpecifiedDeploymentTargets.isEmpty() || userSpecifiedDeploymentTargets.contains(KUBERNETES)) { @@ -121,7 +123,7 @@ public List createConfigurators(KubernetesConfig config, @BuildStep public List createDecorators(ApplicationInfoBuildItem applicationInfo, - OutputTargetBuildItem outputTarget, KubernetesConfig config, PackageConfig packageConfig, + OutputTargetBuildItem outputTarget, Capabilities capabilities, KubernetesConfig config, PackageConfig packageConfig, Optional metricsConfiguration, List annotations, List labels, List envs, Optional image, Optional command, @@ -142,9 +144,17 @@ public List createDecorators(ApplicationInfoBuildItem applic result.addAll(KubernetesCommonHelper.createDecorators(project, KUBERNETES, name, config, metricsConfiguration, annotations, labels, command, ports, livenessPath, readinessPath, roles, roleBindings)); - if (config.deploymentKind == KubernetesConfig.DeploymentResourceKind.StatefulSet) { + KubernetesConfig.DeploymentResourceKind deploymentKind = config.getDeploymentResourceKind(capabilities); + if (deploymentKind != KubernetesConfig.DeploymentResourceKind.Deployment) { result.add(new DecoratorBuildItem(KUBERNETES, new RemoveDeploymentResourceDecorator(name))); + } + + if (deploymentKind == KubernetesConfig.DeploymentResourceKind.StatefulSet) { result.add(new DecoratorBuildItem(KUBERNETES, new AddStatefulSetResourceDecorator(name, config))); + } else if (deploymentKind == KubernetesConfig.DeploymentResourceKind.Job) { + result.add(new DecoratorBuildItem(KUBERNETES, new AddJobResourceDecorator(name, config.job))); + } else if (deploymentKind == KubernetesConfig.DeploymentResourceKind.CronJob) { + result.add(new DecoratorBuildItem(KUBERNETES, new AddCronJobResourceDecorator(name, config.cronJob))); } if (config.ingress != null) { diff --git a/extensions/picocli/runtime/pom.xml b/extensions/picocli/runtime/pom.xml index ea7fc88598ff3f..34d75015952835 100644 --- a/extensions/picocli/runtime/pom.xml +++ b/extensions/picocli/runtime/pom.xml @@ -33,6 +33,11 @@ io.quarkus quarkus-extension-maven-plugin + + + io.quarkus.picocli + + org.jboss.jandex diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceAndMissingScheduleTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceAndMissingScheduleTest.java new file mode 100644 index 00000000000000..68abbb6e24eb41 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceAndMissingScheduleTest.java @@ -0,0 +1,32 @@ + +package io.quarkus.it.kubernetes; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithCronJobResourceAndMissingScheduleTest { + + static final String APP_NAME = "kubernetes-with-cronjob-resource-and-missing-schedule"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .setLogFileName("k8s.log") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()))) + .setExpectedException(IllegalArgumentException.class); + + @Test + public void testShouldNotBeInvokedBecauseMissingScheduleProperty() { + Assertions.fail(); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceTest.java new file mode 100644 index 00000000000000..0b44ec0e142ada --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceTest.java @@ -0,0 +1,74 @@ + +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +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.batch.v1.CronJob; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithCronJobResourceTest { + + static final String APP_NAME = "kubernetes-with-cronjob-resource"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .setLogFileName("k8s.log") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes", 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")); + + assertThat(kubernetesList).filteredOn(i -> i instanceof CronJob).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(CronJob.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + + assertThat(s.getSpec().getSchedule()).isEqualTo("0 0 0 0 *"); + + assertThat(s.getSpec().getJobTemplate().getSpec()).satisfies(jobSpec -> { + assertThat(jobSpec.getParallelism()).isEqualTo(10); + assertThat(jobSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(templateSpec -> { + assertThat(templateSpec.getRestartPolicy()).isEqualTo("Never"); + assertThat(templateSpec.getContainers()).allMatch(c -> { + return APP_NAME.equals(c.getName()) + && c.getArgs().size() == 2 + && c.getArgs().get(0).equals("A") + && c.getArgs().get(1).equals("B"); + }); + }); + }); + assertThat(jobSpec.getSelector()).satisfies(ls -> { + assertThat(ls.getMatchLabels()).containsEntry("app.kubernetes.io/name", APP_NAME); + }); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceTest.java new file mode 100644 index 00000000000000..18e474da12cc1c --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceTest.java @@ -0,0 +1,72 @@ + +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +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.batch.v1.Job; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithJobResourceTest { + + static final String APP_NAME = "kubernetes-with-job-resource"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .setLogFileName("k8s.log") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes", 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")); + + assertThat(kubernetesList).filteredOn(i -> i instanceof Job).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Job.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + + assertThat(s.getSpec()).satisfies(jobSpec -> { + assertThat(jobSpec.getParallelism()).isEqualTo(10); + assertThat(jobSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(templateSpec -> { + assertThat(templateSpec.getRestartPolicy()).isEqualTo("Never"); + assertThat(templateSpec.getContainers()).allMatch(c -> { + return APP_NAME.equals(c.getName()) + && c.getArgs().size() == 2 + && c.getArgs().get(0).equals("A") + && c.getArgs().get(1).equals("B"); + }); + }); + }); + assertThat(jobSpec.getSelector()).satisfies(ls -> { + assertThat(ls.getMatchLabels()).containsEntry("app.kubernetes.io/name", APP_NAME); + }); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceWhenUsingPicocliAndDeploymentTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceWhenUsingPicocliAndDeploymentTest.java new file mode 100644 index 00000000000000..4421b148999781 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceWhenUsingPicocliAndDeploymentTest.java @@ -0,0 +1,55 @@ + +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +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.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithJobResourceWhenUsingPicocliAndDeploymentTest { + + static final String APP_NAME = "kubernetes-with-job-resource-when-using-picocli-and-deployment"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .setLogFileName("k8s.log") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-picocli", 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")); + + assertThat(kubernetesList).filteredOn(i -> i instanceof Deployment).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Deployment.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceWhenUsingPicocliTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceWhenUsingPicocliTest.java new file mode 100644 index 00000000000000..93bcea32d4b708 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithJobResourceWhenUsingPicocliTest.java @@ -0,0 +1,54 @@ + +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +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.batch.v1.Job; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithJobResourceWhenUsingPicocliTest { + + static final String APP_NAME = "kubernetes-with-job-resource-when-using-picocli"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .setLogFileName("k8s.log") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-picocli", 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")); + + assertThat(kubernetesList).filteredOn(i -> i instanceof Job).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Job.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCronJobResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCronJobResourceTest.java new file mode 100644 index 00000000000000..5a4a6d8317415a --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCronJobResourceTest.java @@ -0,0 +1,74 @@ + +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +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.batch.v1.CronJob; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class OpenshiftWithCronJobResourceTest { + + static final String APP_NAME = "openshift-with-cronjob-resource"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .setLogFileName("k8s.log") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-openshift", 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("openshift.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("openshift.yml")); + + assertThat(kubernetesList).filteredOn(i -> i instanceof CronJob).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(CronJob.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + + assertThat(s.getSpec().getSchedule()).isEqualTo("0 0 0 0 *"); + + assertThat(s.getSpec().getJobTemplate().getSpec()).satisfies(jobSpec -> { + assertThat(jobSpec.getParallelism()).isEqualTo(10); + assertThat(jobSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(templateSpec -> { + assertThat(templateSpec.getRestartPolicy()).isEqualTo("Never"); + assertThat(templateSpec.getContainers()).allMatch(c -> { + return APP_NAME.equals(c.getName()) + && c.getArgs().size() == 2 + && c.getArgs().get(0).equals("A") + && c.getArgs().get(1).equals("B"); + }); + }); + }); + assertThat(jobSpec.getSelector()).satisfies(ls -> { + assertThat(ls.getMatchLabels()).containsEntry("app.kubernetes.io/name", APP_NAME); + }); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithJobResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithJobResourceTest.java new file mode 100644 index 00000000000000..2ef31931f10052 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithJobResourceTest.java @@ -0,0 +1,72 @@ + +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +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.batch.v1.Job; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class OpenshiftWithJobResourceTest { + + static final String APP_NAME = "openshift-with-job-resource"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .setLogFileName("k8s.log") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-openshift", 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("openshift.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("openshift.yml")); + + assertThat(kubernetesList).filteredOn(i -> i instanceof Job).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Job.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + + assertThat(s.getSpec()).satisfies(jobSpec -> { + assertThat(jobSpec.getParallelism()).isEqualTo(10); + assertThat(jobSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(templateSpec -> { + assertThat(templateSpec.getRestartPolicy()).isEqualTo("Never"); + assertThat(templateSpec.getContainers()).allMatch(c -> { + return APP_NAME.equals(c.getName()) + && c.getArgs().size() == 2 + && c.getArgs().get(0).equals("A") + && c.getArgs().get(1).equals("B"); + }); + }); + }); + assertThat(jobSpec.getSelector()).satisfies(ls -> { + assertThat(ls.getMatchLabels()).containsEntry("app.kubernetes.io/name", APP_NAME); + }); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource-and-missing-schedule.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource-and-missing-schedule.properties new file mode 100644 index 00000000000000..b821ba209df960 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource-and-missing-schedule.properties @@ -0,0 +1 @@ +quarkus.kubernetes.deployment-kind=CronJob \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource.properties new file mode 100644 index 00000000000000..f812f66973db85 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource.properties @@ -0,0 +1,5 @@ +quarkus.kubernetes.deployment-kind=CronJob +quarkus.kubernetes.arguments=A,B +quarkus.kubernetes.cron-job.schedule=0 0 0 0 * +quarkus.kubernetes.cron-job.parallelism=10 +quarkus.kubernetes.cron-job.restart-policy=Never \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-job-resource-when-using-picocli-and-deployment.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-job-resource-when-using-picocli-and-deployment.properties new file mode 100644 index 00000000000000..b8f038e028af94 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-job-resource-when-using-picocli-and-deployment.properties @@ -0,0 +1 @@ +quarkus.kubernetes.deployment-kind=Deployment \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-job-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-job-resource.properties new file mode 100644 index 00000000000000..936b0ef01b3b3a --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-job-resource.properties @@ -0,0 +1,4 @@ +quarkus.kubernetes.deployment-kind=Job +quarkus.kubernetes.arguments=A,B +quarkus.kubernetes.job.parallelism=10 +quarkus.kubernetes.job.restart-policy=Never \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-cronjob-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-cronjob-resource.properties new file mode 100644 index 00000000000000..c8976a06267538 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-cronjob-resource.properties @@ -0,0 +1,5 @@ +quarkus.openshift.deployment-kind=CronJob +quarkus.openshift.arguments=A,B +quarkus.openshift.cron-job.schedule=0 0 0 0 * +quarkus.openshift.cron-job.parallelism=10 +quarkus.openshift.cron-job.restart-policy=Never \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-job-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-job-resource.properties new file mode 100644 index 00000000000000..034ad33bd6f49d --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-job-resource.properties @@ -0,0 +1,4 @@ +quarkus.openshift.deployment-kind=Job +quarkus.openshift.arguments=A,B +quarkus.openshift.job.parallelism=10 +quarkus.openshift.job.restart-policy=Never \ No newline at end of file