Skip to content

Commit

Permalink
Support generation of Job/CronJob resources
Browse files Browse the repository at this point in the history
Fix #27024
  • Loading branch information
Sgitario authored and gsmet committed Aug 24, 2022
1 parent 709f09d commit 3e4b616
Show file tree
Hide file tree
Showing 28 changed files with 1,141 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
50 changes: 42 additions & 8 deletions docs/src/main/asciidoc/deploying-to-kubernetes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 the https://quarkus.io/guides/deploying-to-kubernetes#quarkus-kubernetes-kubernetes-config_quarkus.kubernetes.job.parallelism-parallelism[Deploying to Kubernetes guide] for more information).

==== Generating CronJob resources

If you want to generate a CronJob resource, you need to add the following properties to 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 provided, the build will fail.

You can configure the rest of the Kubernetes CronJob configuration using the properties under `quarkus.kubernetes.cron-job.xxx` (see the https://quarkus.io/guides/deploying-to-kubernetes#quarkus-kubernetes-kubernetes-config_quarkus.kubernetes.cron-job.parallelism-parallelism[Deploying to Kubernetes guide] for more information).

=== 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`:
Expand Down Expand Up @@ -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]
Expand Down
43 changes: 42 additions & 1 deletion docs/src/main/asciidoc/deploying-to-openshift.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Besides generating a `DeploymentConfig` resource, you can also choose to generate 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`.

Expand All @@ -371,6 +380,38 @@ When the image is built, using OpenShift builds (s2i binary and docker strategy)
quarkus.container-image.group=<project/namespace name>
----

===== 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.openshift.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.openshift.arguments`. For example, by adding the property `quarkus.openshift.arguments=A,B`.

Finally, the Kubernetes job will be launched every time it 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[document].

You can configure the rest of the Kubernetes Job configuration using the properties under `quarkus.openshift.job.xxx` (see the https://quarkus.io/guides/deploying-to-openshift#quarkus-openshift-openshift-config_quarkus.openshift.job.parallelism[Deploying to OpenShift guide] for more information).

===== Generating CronJob resources

If you want to generate a CronJob resource, you need to add the following properties to 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 provided, the build will fail.

You can configure the rest of the Kubernetes CronJob configuration using the properties under `quarkus.openshift.cron-job.xxx` (see the https://quarkus.io/guides/deploying-to-openshift#quarkus-openshift-openshift-config_quarkus.openshift.cron-job.parallelism[Deploying to OpenShift guide] for more information).

==== 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.
Expand Down
98 changes: 92 additions & 6 deletions docs/src/main/asciidoc/picocli.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,26 @@ 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`:
This will add the following to your build file:

[source,xml]
[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-picocli</artifactId>
</dependency>
----

[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
.build.gradle
----
implementation("io.quarkus:quarkus-picocli")
----

== Simple command line application

Simple PicocliApplication with only one `Command` can be created as follows:
Expand Down Expand Up @@ -291,6 +296,87 @@ 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 build file:

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-kubernetes</artifactId>
</dependency>
----

[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
.build.gradle
----
implementation("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]
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<KubernetesDeploymentTargetBuildItem> deploymentTargets,
BuildProducer<KubernetesResourceMetadataBuildItem> resourceMeta) {

DeploymentResourceKind deploymentResourceKind = config.getDeploymentResourceKind();
DeploymentResourceKind deploymentResourceKind = config.getDeploymentResourceKind(capabilities);
deploymentTargets
.produce(
new KubernetesDeploymentTargetBuildItem(OPENSHIFT, deploymentResourceKind.kind,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<KubernetesListFluent<?>> {

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<CronJobBuilder> 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<HasMetadata, CronJobBuilder> replaceExistingCronJobResource(KubernetesListFluent<?> list) {
return metadata -> {
list.removeFromItems(metadata);
return new CronJobBuilder((CronJob) metadata);
};
}

private boolean containsContainerWithName(CronJobFluent.SpecNested<CronJobBuilder> spec) {
var jobTemplate = spec.getJobTemplate();
if (jobTemplate == null
|| jobTemplate.getSpec() == null
|| jobTemplate.getSpec().getTemplate() == null
|| jobTemplate.getSpec().getTemplate().getSpec() == null) {
return false;
}

List<Container> containers = jobTemplate.getSpec().getTemplate().getSpec().getContainers();
return containers == null || containers.stream().anyMatch(c -> name.equals(c.getName()));
}
}
Loading

0 comments on commit 3e4b616

Please sign in to comment.