diff --git a/.gitattributes b/.gitattributes
index bb27001..6e7f2a8 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,4 @@
* text=auto
-*.sh eol=lf
\ No newline at end of file
+*.sh eol=lf
+cron-tick-execute eol=lf
+cron-tick eol=lf
\ No newline at end of file
diff --git a/.github/settings.yml b/.github/settings.yml
index f980fff..945844f 100644
--- a/.github/settings.yml
+++ b/.github/settings.yml
@@ -1,6 +1,6 @@
# https://developer.github.com/v3/repos/#edit
repository:
- name: docker-$$IMAGE_NAME$$
+ name: docker-cron-base
description: ""
homepage: https://homecentr.github.io/
private: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 25d8109..a3d1337 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,7 +6,7 @@ on:
pull_request:
env:
- IMAGE_NAME: "homecentr/$$IMAGE_NAME$$"
+ IMAGE_NAME: "homecentr/cron-base"
jobs:
build:
@@ -30,7 +30,7 @@ jobs:
run: docker build . -t ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.docker_tag }}
- name: Test Docker image
- run: cd tests && sudo gradle test --info -Dimage_tag=${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.docker_tag }}
+ run: cd tests && sudo gradle test --info -Ddocker_image_tag=${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.docker_tag }}
- name: Scan with Phonito Security
uses: phonito/phonito-scanner-action@master
diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml
index 9d34d27..c956302 100644
--- a/.github/workflows/ci_cd.yml
+++ b/.github/workflows/ci_cd.yml
@@ -5,7 +5,7 @@ on:
- master
env:
- IMAGE_NAME: "homecentr/$$IMAGE_NAME$$"
+ IMAGE_NAME: "homecentr/cron-base"
jobs:
build:
@@ -22,6 +22,7 @@ jobs:
uses: codfish/semantic-release-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_URL: "https://api.github.com"
- name: Verify Dockerfile with Hadolint
uses: brpaz/hadolint-action@master
@@ -40,7 +41,7 @@ jobs:
- name: Test Docker image
if: env.RELEASE_VERSION != ''
- run: cd tests && sudo gradle test -Dimage_tag=${{ env.IMAGE_NAME }}:${{ env.RELEASE_VERSION }}
+ run: cd tests && sudo gradle test -Ddocker_image_tag=${{ env.IMAGE_NAME }}:${{ env.RELEASE_VERSION }}
- name: Scan with Phonito Security
if: env.RELEASE_VERSION != ''
diff --git a/.github/workflows/regular_scan.yml b/.github/workflows/regular_scan.yml
index b4ca6d5..b0a3c45 100644
--- a/.github/workflows/regular_scan.yml
+++ b/.github/workflows/regular_scan.yml
@@ -4,7 +4,7 @@ on:
- cron: '0 6 * * *'
env:
- IMAGE_NAME: "homecentr/$$IMAGE_NAME$$"
+ IMAGE_NAME: "homecentr/cron-base"
jobs:
build:
diff --git a/Dockerfile b/Dockerfile
index c3c78df..576874c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1 +1,15 @@
-FROM alpine
\ No newline at end of file
+FROM homecentr/base:2.3.0-alpine
+
+ENV CRON_SCHEDULE=""
+ENV PUSH_GATEWAY_URL=""
+
+# Copy s6 configuration and scripts
+COPY ./fs/ /
+
+RUN rm /etc/crontabs/root && \
+ apk add --no-cache \
+ # Required to push metrics to push gateway
+ curl=7.67.0-r0 \
+ # Required for UUID generation
+ util-linux=2.34-r1 && \
+ chmod a+x /usr/sbin/cron-tick-execute
\ No newline at end of file
diff --git a/README.md b/README.md
index 56b1530..07dc304 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,55 @@
-# HomeCentr - $$IMAGE_NAME$$
-Template repository for Docker container repositories
+[](https://github.com/homecentr/docker-cron-base/graphs/commit-activity)
+[](https://github.com/homecentr/docker-cron-base/labels/bug)
+[](https://hub.docker.com/repository/docker/homecentr/cron-base)
+[](https://hub.docker.com/repository/docker/homecentr/cron-base)
+[](https://hub.docker.com/repository/docker/homecentr/cron-base)
-## Project status
+
+
-## Usage (Docker compose)
-### Env. variables
-### Exposed ports
+# HomeCentr - cron-base
+This docker image is used as base image for all homecentr images which require a cron scheduler but can also be used on its own. The image executes a mounted script at the specified schedule and reports the results into a Prometheus [push-gateway](https://github.com/prometheus/pushgateway) for easy monitoring and alerting.
+
+## Usage
+
+```yml
+version: "3.7"
+services:
+ cron-base:
+ build: .
+ image: homecentr/cron-base
+ restart: unless-stopped
+ environment:
+ CRON_SCHEDULE: "* * * * *" # Run script every minute
+ PUSH_GATEWAY_URL: "http://push_gateway:9091/metrics/job/cron/label-name/label-value"
+ volumes:
+ - ./example/success:/config # must contain cron-tick script
+```
+
+## Environment variables
+
+| Name | Default value | Description |
+|------|---------------|-------------|
+| PUID | 7077 | UID of the user the cron-tick script should be running as. |
+| PGID | 7077 | GID of the group the cron-tick script should be running as. |
+| CRON_SCHEDULE | | [Cron expression](https://crontab.guru/) which defines when/how often the script will be executed. This variable is **mandatory**. |
+| PUSH_GATEWAY_URL | | URL of the [push gateway](https://github.com/prometheus/pushgateway) job where the metrics should be reported. The reporting is skipped if the variable is not set. |
+
+## Exposed ports
+
+The image does not expose any ports.
+
+## Volumes
+
+| Container path | Description |
+|-------------|----------------|
+| /config | Directory containing the script which should be executed, the script must be named `cron-tick`. |
## Security
+The container is regularly scanned for vulnerabilities and updated. Further info can be found in the [Security tab](https://github.com/homecentr/docker-cron-base/security).
+
+### Container user
+The container supports privilege drop. Even though the container starts as root, it will use the permissions only to perform the initial set up. The cron-tick script is executed as UID/GID provided in the PUID and PGID environment variables.
-### Vulnerabilities
\ No newline at end of file
+:warning: Do not change the container user directly using the `user` Docker compose property or using the `--user` argument. This would break the privilege drop logic.
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 980a102..499d1bc 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,15 @@
version: "3.7"
services:
- $$IMAGE_NAME$$:
+ cron:
build: .
- image: homecentr/$$IMAGE_NAME$$
- restart: unless-stopped
\ No newline at end of file
+ image: homecentr/cron-base:local
+ environment:
+ CRON_SCHEDULE: "* * * * *"
+ PUSH_GATEWAY_URL: "http://push_gateway:9091/metrics/job/cron/instance/base"
+ volumes:
+ - ./example/cron-tick:/config/cron-tick
+ #- ./example/cron-tick-fail:/config/cron-tick
+ push_gateway:
+ image: prom/pushgateway
+ ports:
+ - 9091:9091
\ No newline at end of file
diff --git a/example/failure/cron-tick b/example/failure/cron-tick
new file mode 100644
index 0000000..55c8452
--- /dev/null
+++ b/example/failure/cron-tick
@@ -0,0 +1,5 @@
+#!/usr/bin/env ash
+
+sleep 2
+
+exit 10
\ No newline at end of file
diff --git a/example/success/cron-tick b/example/success/cron-tick
new file mode 100644
index 0000000..eba081d
--- /dev/null
+++ b/example/success/cron-tick
@@ -0,0 +1,4 @@
+#!/usr/bin/env ash
+
+
+mktemp -d
\ No newline at end of file
diff --git a/fs/etc/cont-init.d/20-tick-script.sh b/fs/etc/cont-init.d/20-tick-script.sh
new file mode 100644
index 0000000..a311a78
--- /dev/null
+++ b/fs/etc/cont-init.d/20-tick-script.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/with-contenv sh
+
+cp /config/cron-tick /usr/sbin/cron-tick
+chmod a+x /usr/sbin/cron-tick
\ No newline at end of file
diff --git a/fs/etc/cont-init.d/21-crontab.sh b/fs/etc/cont-init.d/21-crontab.sh
new file mode 100644
index 0000000..e4c3b87
--- /dev/null
+++ b/fs/etc/cont-init.d/21-crontab.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/with-contenv sh
+
+if [ "$CRON_SCHEDULE" == "" ]
+then
+ echo "The env. variable CRON_SCHEDULE is not set but is mandatory."
+ exit 1
+fi
+
+USER_NAME=$(getent passwd "$PUID" | cut -d: -f1)
+echo "$CRON_SCHEDULE /usr/sbin/cron-tick-execute" > /etc/crontabs/$USER_NAME
\ No newline at end of file
diff --git a/fs/etc/services.d/cron/finish b/fs/etc/services.d/cron/finish
new file mode 100644
index 0000000..010f8ca
--- /dev/null
+++ b/fs/etc/services.d/cron/finish
@@ -0,0 +1,3 @@
+#!/usr/bin/execlineb -S1
+
+s6-svscanctl -t /var/run/s6/services
\ No newline at end of file
diff --git a/fs/etc/services.d/cron/run b/fs/etc/services.d/cron/run
new file mode 100644
index 0000000..4f3b604
--- /dev/null
+++ b/fs/etc/services.d/cron/run
@@ -0,0 +1,6 @@
+#!/usr/bin/with-contenv sh
+
+# crond must be ALWAYS started as root (requires setpgid)
+# exec s6-setuidgid "$PUID:$PGID" crond -f -d 2
+
+exec crond -f -l 2
\ No newline at end of file
diff --git a/fs/usr/sbin/cron-tick-execute b/fs/usr/sbin/cron-tick-execute
new file mode 100644
index 0000000..00d8194
--- /dev/null
+++ b/fs/usr/sbin/cron-tick-execute
@@ -0,0 +1,35 @@
+#!/usr/bin/env ash
+EXEC_ID=$(uuidgen)
+ALREADY_RUNNING=$(pgrep -f /usr/sbin/cron-tick-execute > /dev/null 2> /dev/null || "YES")
+
+if [ "$ALREADY_RUNNING" == "YES" ]
+then
+ echo "The last execution is still running. Skipping this one..."
+ exit 0
+fi
+
+echo "====== Execution started ($(date), $EXEC_ID) ======="
+
+OUTPUT=$(time -f "TIME_OUTPUT|%x|%e" /usr/sbin/cron-tick 2>&1)
+
+TICK_EXIT_CODE=$(echo "$OUTPUT" | grep "TIME_OUTPUT" | cut -d'|' -f2 | tr -d '[:space:]')
+TICK_DURATION=$(echo "$OUTPUT" | grep "TIME_OUTPUT" | cut -d'|' -f3 | tr -d '[:space:]')
+
+echo "Script exit code: $TICK_EXIT_CODE, execution duration: $TICK_DURATION seconds"
+
+if [ "$PUSH_GATEWAY_URL" == "" ]
+then
+ echo "PUSH_GATEWAY_URL variable not set, skipping pushing metrics to push gateway."
+else
+ echo "Pushing metrics to $PUSH_GATEWAY_URL"
+
+cat <
-
-
-
-
-
-
diff --git a/tests/.idea/.name b/tests/.idea/.name
index 71d0457..4f0a63b 100644
--- a/tests/.idea/.name
+++ b/tests/.idea/.name
@@ -1 +1 @@
-docker-$$IMAGE_NAME$$-tests
\ No newline at end of file
+docker-cron-base-tests
\ No newline at end of file
diff --git a/tests/.idea/compiler.xml b/tests/.idea/compiler.xml
index 098cf64..26bfddf 100644
--- a/tests/.idea/compiler.xml
+++ b/tests/.idea/compiler.xml
@@ -2,8 +2,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/tests/.idea/jarRepositories.xml b/tests/.idea/jarRepositories.xml
index fdc392f..5060e6b 100644
--- a/tests/.idea/jarRepositories.xml
+++ b/tests/.idea/jarRepositories.xml
@@ -16,5 +16,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/.idea/uiDesigner.xml b/tests/.idea/uiDesigner.xml
new file mode 100644
index 0000000..e96534f
--- /dev/null
+++ b/tests/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/.project b/tests/.project
index 215fa44..c3c9c53 100644
--- a/tests/.project
+++ b/tests/.project
@@ -1,6 +1,6 @@
- docker-$$IMAGE_NAME$$-tests
+ docker-cron-base-tests
Project tests created by Buildship.
diff --git a/tests/.settings/org.eclipse.buildship.core.prefs b/tests/.settings/org.eclipse.buildship.core.prefs
index e889521..836b833 100644
--- a/tests/.settings/org.eclipse.buildship.core.prefs
+++ b/tests/.settings/org.eclipse.buildship.core.prefs
@@ -1,2 +1,13 @@
+arguments=
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
+gradle.user.home=
+java.home=C\:/Program Files/Java/jdk1.8.0_231
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=true
+show.console.view=true
+show.executions.view=true
diff --git a/tests/build.gradle b/tests/build.gradle
index 80ae1fa..1534bfb 100644
--- a/tests/build.gradle
+++ b/tests/build.gradle
@@ -9,17 +9,21 @@ sourceCompatibility = 1.8
repositories {
mavenCentral()
+ maven {
+ url "https://dl.bintray.com/homecentr/maven"
+ }
}
dependencies {
testImplementation group: 'junit', name: 'junit', version: '4.13'
- testImplementation "org.testcontainers:testcontainers:1.14.1"
+ testImplementation 'org.testcontainers:testcontainers:1.14.1'
+ testImplementation 'io.homecentr:testcontainers-extensions:1.3.0'
testImplementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.30'
testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.30'
}
test {
- systemProperty 'image_tag', System.getProperty('image_tag')
+ systemProperty 'docker_image_tag', System.getProperty('docker_image_tag')
afterTest { desc, result ->
logger.quiet "Executing test ${desc.name} [${desc.className}] with result: ${result.resultType}"
}
diff --git a/tests/settings.gradle b/tests/settings.gradle
index efa59b6..e365bff 100644
--- a/tests/settings.gradle
+++ b/tests/settings.gradle
@@ -1,2 +1,2 @@
-rootProject.name = 'docker-$$IMAGE_NAME$$-tests'
+rootProject.name = 'docker-cron-base-tests'
diff --git a/tests/src/test/java/ContainerTestBase.java b/tests/src/test/java/ContainerTestBase.java
deleted file mode 100644
index a06567b..0000000
--- a/tests/src/test/java/ContainerTestBase.java
+++ /dev/null
@@ -1,36 +0,0 @@
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.output.Slf4jLogConsumer;
-import org.testcontainers.containers.wait.strategy.Wait;
-
-public abstract class ContainerTestBase {
- private static final Logger logger = LoggerFactory.getLogger(ContainerTestBase.class);
-
- private static GenericContainer _container;
-
- @BeforeClass
- public static void setUp() {
- String dockerImageTag = System.getProperty("image_tag", "homecentr/$$IMAGE_NAME$$");
-
- logger.info("Tested Docker image tag: {}", dockerImageTag);
-
- _container = new GenericContainer<>(dockerImageTag)
- .waitingFor(Wait.forHealthcheck());
-
- _container.start();
- _container.followOutput(new Slf4jLogConsumer(logger));
- }
-
- @AfterClass
- public static void cleanUp() {
- _container.stop();
- _container.close();
- }
-
- protected GenericContainer getContainer() {
- return _container;
- }
-}
\ No newline at end of file
diff --git a/tests/src/test/java/CronWithFailingTickShould.java b/tests/src/test/java/CronWithFailingTickShould.java
new file mode 100644
index 0000000..c72535f
--- /dev/null
+++ b/tests/src/test/java/CronWithFailingTickShould.java
@@ -0,0 +1,63 @@
+import helpers.CronDockerTagResolver;
+import helpers.PushGatewayContainerFactory;
+import io.homecentr.testcontainers.containers.GenericContainerEx;
+import io.homecentr.testcontainers.containers.HttpResponse;
+import io.homecentr.testcontainers.containers.wait.strategy.WaitEx;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.testcontainers.containers.Network;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.time.Duration;
+
+import static io.homecentr.testcontainers.WaitLoop.waitFor;
+import static org.junit.Assert.assertTrue;
+
+public class CronWithFailingTickShould {
+ private static GenericContainerEx _cronContainer;
+ private static GenericContainerEx _pushGatewayContainer;
+ private static Network _network;
+
+ @BeforeClass
+ public static void before() {
+ _network = Network.newNetwork();
+
+ _cronContainer = new GenericContainerEx<>(new CronDockerTagResolver())
+ .withNetwork(_network)
+ .withEnv("CRON_SCHEDULE", "* * * * *")
+ .withEnv("PUSH_GATEWAY_URL", "http://push_gateway:9091/metrics/job/cron/instance/base")
+ .withRelativeFileSystemBind(Paths.get("..", "example", "failure").toString(), "/config")
+ .waitingFor(WaitEx.forS6OverlayStart());
+
+ _pushGatewayContainer = PushGatewayContainerFactory.create(_network);
+
+ _pushGatewayContainer.start();
+ _cronContainer.start();
+ }
+
+ @AfterClass
+ public static void after() {
+ _cronContainer.close();
+ _pushGatewayContainer.close();
+ _network.close();
+ }
+
+ @Test
+ public void executeNextTickScript() throws Exception {
+ // First tick
+ waitFor(Duration.ofSeconds(80), () -> _cronContainer.getLogsAnalyzer().contains("Execution finished"));
+
+ // Second tick
+ waitFor(Duration.ofSeconds(80), () -> _cronContainer.getLogsAnalyzer().contains("Execution finished", 2));
+ }
+
+ @Test
+ public void reportMetricsToPushGateway() throws IOException {
+ HttpResponse response = _pushGatewayContainer.makeHttpRequest(9091, "/metrics");
+
+ assertTrue(response.getResponseContent().contains("exit_code{instance=\"base\",job=\"cron\"} 10"));
+ assertTrue(response.getResponseContent().contains("duration_seconds{instance=\"base\",job=\"cron\"}"));
+ }
+}
diff --git a/tests/src/test/java/CronWithSuccessfulTickShould.java b/tests/src/test/java/CronWithSuccessfulTickShould.java
new file mode 100644
index 0000000..f33942c
--- /dev/null
+++ b/tests/src/test/java/CronWithSuccessfulTickShould.java
@@ -0,0 +1,66 @@
+import helpers.CronDockerTagResolver;
+import helpers.PushGatewayContainerFactory;
+import io.homecentr.testcontainers.containers.GenericContainerEx;
+import io.homecentr.testcontainers.containers.HttpResponse;
+import io.homecentr.testcontainers.containers.wait.strategy.WaitEx;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.Network;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.time.Duration;
+
+import static io.homecentr.testcontainers.WaitLoop.waitFor;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class CronWithSuccessfulTickShould {
+ private static GenericContainerEx _cronContainer;
+ private static GenericContainerEx _pushGatewayContainer;
+ private static Network _network;
+
+ @BeforeClass
+ public static void before() {
+ _network = Network.newNetwork();
+
+ _cronContainer = new GenericContainerEx<>(new CronDockerTagResolver())
+ .withNetwork(_network)
+ .withEnv("CRON_SCHEDULE", "* * * * *")
+ .withEnv("PUSH_GATEWAY_URL", "http://push_gateway:9091/metrics/job/cron/instance/base")
+ .withRelativeFileSystemBind(Paths.get("..", "example", "success").toString(), "/config")
+ .waitingFor(WaitEx.forS6OverlayStart());
+
+ _pushGatewayContainer = PushGatewayContainerFactory.create(_network);
+
+ _pushGatewayContainer.start();
+ _cronContainer.start();
+ }
+
+ @AfterClass
+ public static void after() {
+ _cronContainer.close();
+ _pushGatewayContainer.close();
+ _network.close();
+ }
+
+ @Test
+ public void executeCronTickScript() throws Exception {
+ waitFor(Duration.ofSeconds(80), () -> _cronContainer.getLogsAnalyzer().contains("Execution finished"));
+
+ // Returns exit code 1 if the directory is empty
+ Container.ExecResult execResult = _cronContainer.execInContainer("ash", "-c", "find /tmp -mindepth 1 | read");
+
+ assertEquals(0, execResult.getExitCode());
+ }
+
+ @Test
+ public void reportMetricsToPushGateway() throws IOException {
+ HttpResponse response = _pushGatewayContainer.makeHttpRequest(9091, "/metrics");
+
+ assertTrue(response.getResponseContent().contains("exit_code{instance=\"base\",job=\"cron\"} 0"));
+ assertTrue(response.getResponseContent().contains("duration_seconds{instance=\"base\",job=\"cron\"}"));
+ }
+}
diff --git a/tests/src/test/java/CronWithoutApiGatewayShould.java b/tests/src/test/java/CronWithoutApiGatewayShould.java
new file mode 100644
index 0000000..5147561
--- /dev/null
+++ b/tests/src/test/java/CronWithoutApiGatewayShould.java
@@ -0,0 +1,37 @@
+import helpers.CronDockerTagResolver;
+import io.homecentr.testcontainers.containers.GenericContainerEx;
+import io.homecentr.testcontainers.containers.wait.strategy.WaitEx;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.nio.file.Paths;
+import java.time.Duration;
+
+import static io.homecentr.testcontainers.WaitLoop.waitFor;
+
+public class CronWithoutApiGatewayShould {
+ private static GenericContainerEx _cronContainer;
+
+ @BeforeClass
+ public static void before() {
+ _cronContainer = new GenericContainerEx<>(new CronDockerTagResolver())
+ .withEnv("CRON_SCHEDULE", "* * * * *")
+ .withEnv("PUSH_GATEWAY_URL", "http://push_gateway:9091/metrics/job/cron/instance/base")
+ .withRelativeFileSystemBind(Paths.get("..", "example", "success").toString(), "/config")
+ .waitingFor(WaitEx.forS6OverlayStart());
+
+ _cronContainer.start();
+ }
+
+ @AfterClass
+ public static void after() {
+ _cronContainer.close();
+ }
+
+ @Test
+ public void executeNextTickScript() throws Exception {
+ waitFor(Duration.ofSeconds(80), () -> _cronContainer.getLogsAnalyzer().contains("Execution finished"));
+ waitFor(Duration.ofSeconds(80), () -> _cronContainer.getLogsAnalyzer().contains("Execution started", 2));
+ }
+}
\ No newline at end of file
diff --git a/tests/src/test/java/helpers/CronDockerTagResolver.java b/tests/src/test/java/helpers/CronDockerTagResolver.java
new file mode 100644
index 0000000..b23e2ff
--- /dev/null
+++ b/tests/src/test/java/helpers/CronDockerTagResolver.java
@@ -0,0 +1,9 @@
+package helpers;
+
+import io.homecentr.testcontainers.images.EnvironmentImageTagResolver;
+
+public class CronDockerTagResolver extends EnvironmentImageTagResolver {
+ public CronDockerTagResolver() {
+ super("homecentr/cron-base:local");
+ }
+}
diff --git a/tests/src/test/java/helpers/PushGatewayContainerFactory.java b/tests/src/test/java/helpers/PushGatewayContainerFactory.java
new file mode 100644
index 0000000..d5413f0
--- /dev/null
+++ b/tests/src/test/java/helpers/PushGatewayContainerFactory.java
@@ -0,0 +1,16 @@
+package helpers;
+
+import io.homecentr.testcontainers.containers.GenericContainerEx;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+public class PushGatewayContainerFactory {
+ public static GenericContainerEx create(Network network) {
+ GenericContainerEx result = new GenericContainerEx<>("prom/pushgateway")
+ .withNetwork(network)
+ .withNetworkAliases("push_gateway")
+ .waitingFor(Wait.forLogMessage(".*listen_address=:9091.*", 1));
+
+ return result;
+ }
+}