Skip to content

Commit

Permalink
fix: suspend specific cronJob when deactivation action is requested &…
Browse files Browse the repository at this point in the history
… block automatic cronjob creation on a specific namespace (#97)

* fix: suspend specific cronJob when deactivation action is requested

* fix: block automatic cronjob creation on a specific namespace

---------

Co-authored-by: Franck Braffouo
  • Loading branch information
Franck-Aymar-Braffouo authored Feb 25, 2025
1 parent b83630f commit 47df104
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 65 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [About](#about)
- [Getting Started](#getting-started)
- [How Does It Work](#how-does-it-work)
- [Configure your DailyClean](#configure-your-dailyclean)
- [Contribute](#contribute)
- [Authors](#authors)

Expand Down Expand Up @@ -67,6 +68,12 @@ metadata:
axa.com/function: 'true'
```
## Configure your DailyClean
| Environment Variable | Description | Default value |
|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `SERVICE_UNAUTHORIZED_NAMESPACE_REGEX` | If needed, it is possible to specify a regex matching the Kubernetes namespaces on which the automatic start or stop actions should not be applied | |

## Contribute

- [How to run the solution and to contribute](./CONTRIBUTING.md)
Expand Down
19 changes: 19 additions & 0 deletions api/api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>3.17.3</quarkus.platform.version>
<surefire-plugin.version>3.5.2</surefire-plugin.version>
<jacoco.version>0.8.12</jacoco.version>
<package>fr.axa.openpaas.dailyclean</package>
</properties>
<dependencyManagement>
Expand Down Expand Up @@ -141,6 +142,24 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<exclClassLoaders>*QuarkusClassLoader</exclClassLoaders>
<destFile>${project.build.directory}/jacoco-quarkus.exec</destFile>
<append>true</append>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import fr.axa.openpaas.dailyclean.model.Workload;
import fr.axa.openpaas.dailyclean.util.KubernetesUtils;
import fr.axa.openpaas.dailyclean.util.NamespaceVerifier;
import fr.axa.openpaas.dailyclean.util.wrapper.DeploymentWrapper;
import fr.axa.openpaas.dailyclean.util.wrapper.StatefulSetWrapper;
import io.fabric8.kubernetes.api.model.Container;
Expand All @@ -18,6 +19,7 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.InternalServerErrorException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static fr.axa.openpaas.dailyclean.service.KubernetesArgument.START;
Expand All @@ -44,6 +46,9 @@ public class KubernetesService {
@ConfigProperty(name = "service.deployment.label.dailyclean")
String dailycleanLabelName;

@ConfigProperty(name = "service.unauthorized.namespace.regex")
Optional<String> unauthorizedNamespaceRegex;

private final KubernetesClient kubernetesClient;

public KubernetesService(final KubernetesClient kubernetesClient) {
Expand All @@ -56,9 +61,10 @@ public KubernetesService(final KubernetesClient kubernetesClient) {
* @param cron The cron given by the end user.
*/
public void createStartCronJob(String cron) {
if(StringUtils.isNotBlank(cron)) {
createCronJob(cron, START);
}
Boolean suspended = KubernetesUtils.getCorrectSuspendValue(cron);
String cronExpressionToApply = KubernetesUtils.geCorrectCronExpression(cron);

createCronJob(cronExpressionToApply, suspended, START);
}

/**
Expand All @@ -67,9 +73,10 @@ public void createStartCronJob(String cron) {
* @param cron The cron given by the end user.
*/
public void createStopCronJob(String cron) {
if(StringUtils.isNotBlank(cron)) {
createCronJob(cron, STOP);
}
Boolean suspended = KubernetesUtils.getCorrectSuspendValue(cron);
String cronExpressionToApply = KubernetesUtils.geCorrectCronExpression(cron);

createCronJob(cronExpressionToApply, suspended, STOP);
}

/**
Expand Down Expand Up @@ -155,7 +162,7 @@ public void updatingCronJobIfNeeded() {
public void createDefaultStopCronJobIfNotExist() {
CronJob stop = getCronJob(STOP);
if(stop == null) {
createCronJob(defaultCronStop, STOP);
createCronJob(defaultCronStop, false, STOP);
}
}

Expand Down Expand Up @@ -183,7 +190,11 @@ private String getCronAsString(KubernetesArgument argument) {
}

private String getCronAsStringFromCronJob(CronJob cronJob) {
return cronJob != null ? cronJob.getSpec().getSchedule() : null;
return cronJobIsNotSuspended(cronJob) ? cronJob.getSpec().getSchedule() : null;
}

private boolean cronJobIsNotSuspended(final CronJob cronJob) {
return cronJob != null && !cronJob.getSpec().getSuspend();
}

private String getContainerImageName(CronJob cronJob) {
Expand All @@ -200,13 +211,15 @@ private String getContainerImageName(CronJob cronJob) {
return res;
}

private void createCronJob(String cron, KubernetesArgument argument) {
private void createCronJob(String cron, Boolean suspend, KubernetesArgument argument) {
assertImgNameIsNotBlanck();
assertThatNamespaceIsAuthorized();

final String namespace = getNamespace();

logger.info("Creating cron job from object");
kubernetesClient.batch().v1().cronjobs().inNamespace(namespace)
.load(KubernetesUtils.createCronJobAsInputStream(argument, cron, imgName, serviceAccountName, timeZone))
.load(KubernetesUtils.createCronJobAsInputStream(argument, cron, imgName, serviceAccountName, timeZone, suspend))
.createOrReplace();
logger.info("Successfully created cronjob with name {}", KubernetesUtils.getCronName(argument));
}
Expand Down Expand Up @@ -236,6 +249,16 @@ private void assertImgNameIsNotBlanck() {
}
}

private void assertThatNamespaceIsAuthorized() {
final String currentNamespace = kubernetesClient.getNamespace();

if(unauthorizedNamespaceRegex.isPresent() &&
NamespaceVerifier.isNotAuthorize(currentNamespace, unauthorizedNamespaceRegex.get())){
throw new InternalServerErrorException(
"Create CronJob action is not authorized for this namespace. Actual regex for unauthorized namespace : %s".formatted(unauthorizedNamespaceRegex.get()));
}
}

private void deleteCronJobIfExists(KubernetesArgument argument, String namespace) {
Resource<CronJob> cronJob =
kubernetesClient.batch().v1().cronjobs()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
package fr.axa.openpaas.dailyclean.util;

import static fr.axa.openpaas.dailyclean.util.ScriptPlaceholder.*;

import fr.axa.openpaas.dailyclean.model.Container;
import fr.axa.openpaas.dailyclean.model.Workload;
import fr.axa.openpaas.dailyclean.model.Port;
import fr.axa.openpaas.dailyclean.model.Resource;
import fr.axa.openpaas.dailyclean.model.Workload;
import fr.axa.openpaas.dailyclean.service.KubernetesArgument;
import fr.axa.openpaas.dailyclean.util.wrapper.IWorkloadWrapper;
import io.fabric8.kubernetes.api.model.ContainerPort;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.api.model.apps.DeploymentSpec;
import io.fabric8.kubernetes.api.model.apps.DeploymentStatus;
import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec;
import io.fabric8.kubernetes.api.model.apps.StatefulSetStatus;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
Expand All @@ -29,10 +24,14 @@
import java.util.Map;
import java.util.stream.Collectors;

import static fr.axa.openpaas.dailyclean.util.ScriptPlaceholder.*;

public final class KubernetesUtils {

public static final String CRON_JOB_NAME = "dailyclean";
public static final String JOB_NAME = "dailyclean-job";
public static final String DEFAULT_CRON_WHEN_JOB_IS_SUSPENDED = "0 0 * * *";


private KubernetesUtils() {}

Expand All @@ -47,14 +46,16 @@ public static InputStream createCronJobAsInputStream(KubernetesArgument argument
String cron,
String imgName,
String serviceAccountName,
String timeZone) {
String timeZone,
Boolean suspend) {
String text = getFileAsString("scripts/cronjob.yml");
String cronJobAsdString = text.replace(NAME.getPlaceholder(), getCronName(argument))
.replace(ARGUMENT.getPlaceholder(), argument.getValue())
.replace(SCHEDULE.getPlaceholder(), cron)
.replace(IMG_NAME.getPlaceholder(), imgName)
.replace(SERVICE_ACCOUNT_NAME.getPlaceholder(), serviceAccountName)
.replace(TIME_ZONE.getPlaceholder(), timeZone);
.replace(TIME_ZONE.getPlaceholder(), timeZone)
.replace(SUSPEND.getPlaceholder(), Boolean.toString(suspend));

return new ByteArrayInputStream(cronJobAsdString.getBytes());
}
Expand Down Expand Up @@ -84,6 +85,17 @@ public static String getJobName(KubernetesArgument argument) {
return getName(argument, JOB_NAME);
}

public static String geCorrectCronExpression(final String cron) {
if(StringUtils.isBlank(cron)) {
return DEFAULT_CRON_WHEN_JOB_IS_SUSPENDED;
}
return cron;
}

public static boolean getCorrectSuspendValue(String cron) {
return StringUtils.isBlank(cron);
}

public static Workload mapWorkload(IWorkloadWrapper workload,
String dailycleanLabelName) {
Workload res = new Workload();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package fr.axa.openpaas.dailyclean.util;

import org.apache.commons.lang3.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class NamespaceVerifier {

public static boolean isNotAuthorize(final String namespace, final String regex) {
if(StringUtils.isBlank(regex)){
return false;
}

Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(namespace);

return matcher.find();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public enum ScriptPlaceholder {
SCHEDULE("{{schedule}}"),
IMG_NAME("{{imgName}}"),
SERVICE_ACCOUNT_NAME("{{serviceAccountName}}"),
TIME_ZONE("{{timeZone}}"),;
TIME_ZONE("{{timeZone}}"),
SUSPEND("{{suspend}}");

private String placeholder;
ScriptPlaceholder(String placeholder) {
Expand Down
1 change: 1 addition & 0 deletions api/api/src/main/resources/scripts/cronjob.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ spec:
concurrencyPolicy: Forbid
schedule: '{{schedule}}'
timeZone: '{{timeZone}}'
suspend: '{{suspend}}'
jobTemplate:
spec:
template:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public abstract class AbstractTimeRangesResourceTest {
protected static final String CRON_19_00 = "0 19 * * *";
protected static final String CRON_10_00 = "0 10 * * *";
protected static final String CRON_20_00 = "0 20 * * *";
protected static final String DEFAULT_SUSPENDED_CRON = "0 0 * * *";
protected static final String IMG_NAME = "axaguildev/dailyclean-job:latest";
protected static final String SERVICE_ACCOUNT_NAME = "default";
protected static final String TIME_ZONE = "UTC";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ void shouldNotCreateCronStopJobWithDefaultWhenItNotExists() {

private static void createCronJobStop(KubernetesClient client, String namespace) {
InputStream cronJobAsInputStream =
KubernetesUtils.createCronJobAsInputStream(STOP, CRON_10_00, "imagename:1.0.0", "default", "UTC");
KubernetesUtils.createCronJobAsInputStream(STOP, CRON_10_00, "imagename:1.0.0", "default", "UTC", false);
client.load(cronJobAsInputStream).inNamespace(namespace).createOrReplace();
}
}
Loading

0 comments on commit 47df104

Please sign in to comment.