Skip to content

Commit

Permalink
feat(jkube-kit): Initial Support for CronJob (eclipse-jkube#1954)
Browse files Browse the repository at this point in the history
Signed-off-by: Anurag Rajawat <[email protected]>
  • Loading branch information
anurag-rajawat committed Mar 27, 2023
1 parent 48ebbeb commit d049be9
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Usage:
### 1.12-SNAPSHOT
* Fix #1179: Move storageClass related functionality out of VolumePermissionEnricher to PersistentVolumeClaimStorageClassEnricher
* Fix #1273: Deprecate `jkube.io` annotation prefix in favor of `jkube.eclipse.org` for JKubeAnnotations
* Fix #1954: Add support for CronJob
* Fix #2079: Bump kubernetes-client to 6.5.0
* Fix #2093: ClassCastException when extracting plugins from pom

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public class KubernetesHelper {
public static final Pattern FILENAME_PATTERN = Pattern.compile(FILENAME_PATTERN_REGEX, Pattern.CASE_INSENSITIVE);
public static final Pattern PROFILES_PATTERN = Pattern.compile(PROFILES_PATTERN_REGEX, Pattern.CASE_INSENSITIVE);
protected static final String[] POD_CONTROLLER_KINDS =
{ "ReplicationController", "ReplicaSet", "Deployment", "DeploymentConfig", "StatefulSet", "DaemonSet", "Job" };
{ "ReplicationController", "ReplicaSet", "Deployment", "DeploymentConfig", "StatefulSet", "DaemonSet", "Job", "CronJob" };

private KubernetesHelper() {}

Expand All @@ -121,6 +121,13 @@ public static String validateKubernetesId(String currentValue, String descriptio
return currentValue;
}

public static String validateCronJobSchedule(String schedule) {
if (StringUtils.isBlank(schedule)) {
throw new IllegalArgumentException("No schedule is specified!");
}
return schedule;
}

public static Map<String, String> getOrCreateAnnotations(HasMetadata entity) {
ObjectMeta metadata = getOrCreateMetadata(entity);
Map<String, String> answer = metadata.getAnnotations();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ public class ControllerResourceConfig {
private String imagePullPolicy;
private Integer replicas;
private String restartPolicy;
private String schedule;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2019 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at:
*
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.jkube.kit.enricher.handler;

import java.util.List;
import java.util.Optional;

import org.eclipse.jkube.kit.common.util.KubernetesHelper;
import org.eclipse.jkube.kit.config.image.ImageConfiguration;
import org.eclipse.jkube.kit.config.resource.ControllerResourceConfig;

import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
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.CronJobSpec;
import io.fabric8.kubernetes.api.model.batch.v1.CronJobSpecBuilder;
import io.fabric8.kubernetes.api.model.batch.v1.JobSpec;
import io.fabric8.kubernetes.api.model.batch.v1.JobSpecBuilder;
import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpec;
import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpecBuilder;

public class CronJobHandler implements ControllerHandler<CronJob> {
private static final String DEFAULT_JOB_RESTART_POLICY = "OnFailure";
private final PodTemplateHandler podTemplateHandler;

public CronJobHandler(PodTemplateHandler podTemplateHandler) {
this.podTemplateHandler = podTemplateHandler;
}

@Override
public CronJob get(ControllerResourceConfig config, List<ImageConfiguration> images) {
return new CronJobBuilder()
.withMetadata(createCronJobMetadata(config))
.withSpec(createCronJobSpec(config, images))
.build();
}

@Override
public PodTemplateSpec getPodTemplateSpec(ControllerResourceConfig config, List<ImageConfiguration> images) {
return get(config, images).getSpec().getJobTemplate().getSpec().getTemplate();
}

@Override
public PodTemplateSpec getPodTemplate(CronJob controller) {
return controller.getSpec().getJobTemplate().getSpec().getTemplate();
}

@Override
public void overrideReplicas(KubernetesListBuilder resources, int replicas) {
// NOOP
}

private ObjectMeta createCronJobMetadata(ControllerResourceConfig config) {
return new ObjectMetaBuilder()
.withName(KubernetesHelper.validateKubernetesId(config.getControllerName(), "controller name"))
.build();
}

private CronJobSpec createCronJobSpec(ControllerResourceConfig config, List<ImageConfiguration> images) {
return new CronJobSpecBuilder()
.withSchedule(KubernetesHelper.validateCronJobSchedule(config.getSchedule()))
.withJobTemplate(createJobTemplateSpec(config, images))
.build();
}

private JobTemplateSpec createJobTemplateSpec(ControllerResourceConfig config, List<ImageConfiguration> images) {
return new JobTemplateSpecBuilder()
.withSpec(createJobSpec(config, images))
.build();
}

private JobSpec createJobSpec(ControllerResourceConfig config, List<ImageConfiguration> images) {
return new JobSpecBuilder()
.withTemplate(podTemplateHandler.getPodTemplate(config,
Optional.ofNullable(config.getRestartPolicy()).orElse(DEFAULT_JOB_RESTART_POLICY), images))
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.fabric8.kubernetes.api.model.apps.ReplicaSet;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.api.model.batch.v1.Job;
import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
import io.fabric8.openshift.api.model.DeploymentConfig;

/**
Expand Down Expand Up @@ -57,7 +58,8 @@ public HandlerHub(GroupArtifactVersion groupArtifactVersion, Properties configur
new ReplicationControllerHandler(podTemplateHandler)),
new ControllerHandlerLazyBuilder<>(StatefulSet.class,() -> new StatefulSetHandler(podTemplateHandler)),
new ControllerHandlerLazyBuilder<>(DaemonSet.class,() -> new DaemonSetHandler(podTemplateHandler)),
new ControllerHandlerLazyBuilder<>(Job.class,() -> new JobHandler(podTemplateHandler))
new ControllerHandlerLazyBuilder<>(Job.class,() -> new JobHandler(podTemplateHandler)),
new ControllerHandlerLazyBuilder<>(CronJob.class, () -> new CronJobHandler(podTemplateHandler))
);
namespaceHandler = new LazyBuilder<>(NamespaceHandler::new);
projectHandler = new LazyBuilder<>(ProjectHandler::new);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,4 @@ private List<Volume> getVolumes(ControllerResourceConfig config) {
return ret;
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Copyright (c) 2019 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at:
*
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.jkube.kit.enricher.handler;

import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
import io.fabric8.kubernetes.api.model.batch.v1.JobSpec;
import org.eclipse.jkube.kit.config.image.ImageConfiguration;
import org.eclipse.jkube.kit.config.image.build.BuildConfiguration;
import org.eclipse.jkube.kit.config.resource.ControllerResourceConfig;
import org.eclipse.jkube.kit.config.resource.GroupArtifactVersion;
import org.eclipse.jkube.kit.config.resource.VolumeConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

class CronJobHandlerTest {
private List<VolumeConfig> volumes;
private List<ImageConfiguration> images;
private CronJobHandler cronJobHandler;

@BeforeEach
void setUp() {
volumes = new ArrayList<>();
images = new ArrayList<>();
List<String> mounts = new ArrayList<>();
List<String> ports = new ArrayList<>();
List<String> tags = new ArrayList<>();

//volume config with name and multiple mount
mounts.add("/path/system");
mounts.add("/path/sys");

ports.add("8080");
ports.add("9090");

tags.add("latest");
tags.add("test");

VolumeConfig volumeConfig = VolumeConfig.builder()
.name("test").mounts(mounts).type("hostPath").path("/test/path").build();
volumes.add(volumeConfig);

//container name with alias
final BuildConfiguration buildImageConfiguration = BuildConfiguration.builder()
.ports(ports).from("fabric8/maven:latest").cleanup("try")
.tags(tags).compressionString("gzip").build();

ImageConfiguration imageConfiguration = ImageConfiguration.builder()
.name("test").alias("test-app").build(buildImageConfiguration)
.registry("docker.io").build();
images.add(imageConfiguration);

cronJobHandler = new CronJobHandler(new PodTemplateHandler(new ContainerHandler(new Properties(),
new GroupArtifactVersion("g", "a", "v"), new ProbeHandler())));
}

@Test
void get_withValidConfigs_shouldReturnConfigWithContainers() {
// Given
ControllerResourceConfig config = ControllerResourceConfig.builder()
.schedule("* * * * *")
.controllerName("testing")
.restartPolicy("Never")
.volumes(volumes)
.build();

// When
CronJob cronJob = cronJobHandler.get(config, images);

// Then
assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getSpec().getContainers())
.isNotNull();
assertThat(cronJob)
.hasFieldOrPropertyWithValue("metadata.name", "testing")
.hasFieldOrPropertyWithValue("spec.schedule", "* * * * *")
.satisfies(cj -> assertThat(cj.getSpec().getJobTemplate().getSpec())
.isNotNull()
.extracting(JobSpec::getTemplate)
.extracting(PodTemplateSpec::getSpec)
.hasFieldOrPropertyWithValue("restartPolicy", "Never")
.extracting(PodSpec::getVolumes).asList().first()
.hasFieldOrPropertyWithValue("name", "test")
.hasFieldOrPropertyWithValue("hostPath.path", "/test/path")
);
}

@Test
void get_withoutSchedule_shouldThrowException() {
// Given
ControllerResourceConfig config = ControllerResourceConfig.builder()
.controllerName("testing")
.restartPolicy("Never")
.volumes(volumes)
.build();

// When & Then
assertThatIllegalArgumentException()
.isThrownBy(() -> cronJobHandler.get(config, images))
.withMessage("No schedule is specified!");
}

@Test
void get_withInvalidControllerName_shouldThrowException() {
// Given
ControllerResourceConfig config = ControllerResourceConfig.builder()
.schedule("* * * * *")
.imagePullPolicy("IfNotPresent")
.controllerName("TesTing")
.volumes(volumes)
.build();
// When & Then
assertThatIllegalArgumentException()
.isThrownBy(() -> cronJobHandler.get(config, images))
.withMessageStartingWith("Invalid upper case letter 'T'")
.withMessageEndingWith("controller name value: TesTing");
}

@Test
void get_withoutControllerName_shouldThrowException() {
// Given
ControllerResourceConfig config = ControllerResourceConfig.builder()
.schedule("* * * * *")
.imagePullPolicy("IfNotPresent")
.volumes(volumes)
.build();
// When & Then
assertThatIllegalArgumentException()
.isThrownBy(() -> cronJobHandler.get(config, images))
.withMessage("No controller name is specified!");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ void getControllerHandlers_shouldReturnAllImplementationsOfControllerHandler() {
final List<ControllerHandlerLazyBuilder<?>> result = handlerHub.getControllerHandlers();
// Then
assertThat(result)
.hasSize(7)
.hasSize(8)
.extracting(ControllerHandlerLazyBuilder::get)
.flatExtracting(Object::getClass)
.containsExactlyInAnyOrder(
Expand All @@ -47,7 +47,8 @@ void getControllerHandlers_shouldReturnAllImplementationsOfControllerHandler() {
JobHandler.class,
ReplicaSetHandler.class,
ReplicationControllerHandler.class,
StatefulSetHandler.class
StatefulSetHandler.class,
CronJobHandler.class
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

public class ControllerViaPluginConfigurationEnricher extends BaseEnricher {
protected static final String[] POD_CONTROLLER_KINDS =
{ "ReplicationController", "ReplicaSet", "Deployment", "DeploymentConfig", "StatefulSet", "DaemonSet", "Job" };
{ "ReplicationController", "ReplicaSet", "Deployment", "DeploymentConfig", "StatefulSet", "DaemonSet", "Job", "CronJob" };

private final ControllerHandler<Deployment> deployHandler;
private final ControllerHandler<StatefulSet> statefulSetHandler;
Expand All @@ -57,7 +57,8 @@ private enum Config implements Configs.Config {
*/
@Deprecated
PULL_POLICY("pullPolicy", JKUBE_DEFAULT_IMAGE_PULL_POLICY),
REPLICA_COUNT("replicaCount", "1");
REPLICA_COUNT("replicaCount", "1"),
SCHEDULE("schedule", null);

@Getter
protected String key;
Expand All @@ -79,6 +80,7 @@ public void create(PlatformMode platformMode, KubernetesListBuilder builder) {
.imagePullPolicy(getImagePullPolicy(Config.PULL_POLICY))
.replicas(getReplicaCount(builder, Configs.asInt(getConfig(Config.REPLICA_COUNT))))
.initContainers(Optional.ofNullable(getControllerResourceConfig().getInitContainers()).orElse(Collections.emptyList()))
.schedule(getConfig(Config.SCHEDULE))
.build();

final List<ImageConfiguration> images = getImages();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.ReplicaSet;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
import io.fabric8.kubernetes.api.model.batch.v1.Job;
import org.eclipse.jkube.kit.common.Configs;
import org.eclipse.jkube.kit.common.util.JKubeProjectUtil;
Expand Down Expand Up @@ -57,6 +58,7 @@
* <li>ReplicaSet</li>
* <li>ReplicationController</li>
* <li>Job</li>
* <li>CronJob</li>
* </ul>
*
* TODO: There is a certain overlap with the ImageEnricher with adding default images etc.. This must be resolved.
Expand All @@ -74,6 +76,7 @@ public class DefaultControllerEnricher extends BaseEnricher {
CONTROLLER_TYPES.put("REPLICASET", ReplicaSet.class);
CONTROLLER_TYPES.put("REPLICATIONCONTROLLER", ReplicationController.class);
CONTROLLER_TYPES.put("JOB", Job.class);
CONTROLLER_TYPES.put("CRONJOB", CronJob.class);
}

@AllArgsConstructor
Expand All @@ -85,7 +88,8 @@ public enum Config implements Configs.Config {
@Deprecated
PULL_POLICY("pullPolicy", JKUBE_DEFAULT_IMAGE_PULL_POLICY),
TYPE("type", null),
REPLICA_COUNT("replicaCount", "1");
REPLICA_COUNT("replicaCount", "1"),
SCHEDULE("schedule", null);

@Getter
protected String key;
Expand All @@ -106,6 +110,7 @@ public void create(PlatformMode platformMode, KubernetesListBuilder builder) {
.imagePullPolicy(getImagePullPolicy(Config.PULL_POLICY))
.replicas(getReplicaCount(builder, Configs.asInt(getConfig(Config.REPLICA_COUNT))))
.restartPolicy(getControllerResourceConfig().getRestartPolicy())
.schedule(getConfig(Config.SCHEDULE))
.build();

final List<ImageConfiguration> images = getImages();
Expand Down

0 comments on commit d049be9

Please sign in to comment.