Skip to content

Commit d96c899

Browse files
authored
Merge pull request #7 from joon6093/1.2.x
Release v1.2.1
2 parents 69ec5e6 + f3bc425 commit d96c899

File tree

10 files changed

+204
-51
lines changed

10 files changed

+204
-51
lines changed

handler/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = 'io.jeyong'
8-
version = '1.2.0'
8+
version = '1.2.1'
99

1010
ext {
1111
artifactName = 'k8s-sigterm-handler'

handler/src/main/java/io/jeyong/handler/ApplicationTerminator.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,19 @@
44

55
public abstract class ApplicationTerminator {
66

7+
private final String terminationMessagePath;
8+
private final String terminationMessage;
9+
10+
protected ApplicationTerminator(final String terminationMessagePath, final String terminationMessage) {
11+
this.terminationMessagePath = terminationMessagePath;
12+
this.terminationMessage = terminationMessage;
13+
}
14+
715
public SignalHandler handleTermination() {
8-
return signal -> System.exit(getExitCode());
16+
return signal -> {
17+
FileUtils.writeToFile(terminationMessagePath, terminationMessage);
18+
System.exit(getExitCode());
19+
};
920
}
1021

1122
protected abstract int getExitCode();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.jeyong.handler;
2+
3+
import java.io.File;
4+
import java.io.FileWriter;
5+
import java.io.IOException;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
public class FileUtils {
10+
11+
private static final Logger logger = LoggerFactory.getLogger(FileUtils.class);
12+
13+
private FileUtils() {
14+
}
15+
16+
public static void writeToFile(final String filePath, final String message) {
17+
if (filePath == null || filePath.isBlank()) {
18+
return;
19+
}
20+
21+
final File file = new File(filePath);
22+
try {
23+
createParentDirectories(file);
24+
try (FileWriter writer = new FileWriter(file)) {
25+
writer.write(message);
26+
}
27+
} catch (IOException e) {
28+
logger.error("Failed to write to file {}: {}", filePath, e.getMessage());
29+
}
30+
}
31+
32+
private static void createParentDirectories(final File file) throws IOException {
33+
final File parentDir = file.getParentFile();
34+
if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) {
35+
throw new IOException("Failed to create parent directories for " + file.getAbsolutePath());
36+
}
37+
}
38+
}

handler/src/main/java/io/jeyong/handler/SigtermHandlerConfiguration.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
@Configuration
1010
@ConditionalOnProperty(
11-
prefix = "kubernetes.handler",
11+
prefix = "kubernetes.sigterm-handler",
1212
name = "enabled",
1313
havingValue = "true",
1414
matchIfMissing = true
@@ -26,7 +26,8 @@ public SigtermHandlerConfiguration(final SigtermHandlerProperties sigtermHandler
2626

2727
@Bean
2828
public ApplicationTerminator applicationTerminator(final ApplicationContext applicationContext) {
29-
return new SpringContextTerminator(applicationContext, sigtermHandlerProperties.getExitCode());
29+
return new SpringContextTerminator(applicationContext, sigtermHandlerProperties.getExitCode(),
30+
sigtermHandlerProperties.getTerminationMessagePath(), sigtermHandlerProperties.getTerminationMessage());
3031
}
3132

3233
@Bean
Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,62 @@
11
package io.jeyong.handler;
22

3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
35
// @formatter:off
4-
import org.springframework.boot.context.properties.ConfigurationProperties; /**
6+
/**
57
* Configuration properties for Kubernetes SIGTERM handling.
68
*
79
* <p>
8-
* Allows customization of the SIGTERM handler's behavior, including whether it is enabled and the exit code to use
9-
* during graceful termination.
10+
* Allows customization of the Sigterm Handler's behavior, including whether it is enabled,
11+
* the exit code to use during graceful termination, and optional termination message settings.
1012
* </p>
1113
*
1214
* <ul>
13-
* <li><b>kubernetes.handler.enabled:</b> Set whether the handler is enabled or disabled (default: true).</li>
14-
* <li><b>kubernetes.handler.exit-code:</b> Sets the exit code for graceful application termination (default: 0).</li>
15+
* <li><b>kubernetes.sigterm-handler.enabled:</b> Set whether the handler is enabled or disabled. (default: true)</li>
16+
* <li><b>kubernetes.sigterm-handler.exit-code:</b> Set the exit code for graceful application termination. (default: 0)</li>
17+
* <li><b>kubernetes.sigterm-handler.termination-message-path:</b> Set the file path where the termination message should be written. (default: not set)</li>
18+
* <li><b>kubernetes.sigterm-handler.termination-message:</b> Set the content of the termination message written to the specified path. (default: SIGTERM signal received. Application has been terminated successfully.)</li>
1519
* </ul>
1620
*
1721
* <pre>
1822
* Example configuration (YAML):
1923
* {@code
2024
* kubernetes:
21-
* handler:
25+
* sigterm-handler:
2226
* enabled: true
23-
* exit-code: 1
27+
* exit-code: 0
28+
* termination-message-path: /dev/termination-log
29+
* termination-message: SIGTERM signal received...
2430
* }
2531
* </pre>
2632
*
2733
* <pre>
2834
* Example configuration (Properties):
2935
* {@code
30-
* kubernetes.handler.enabled=true
31-
* kubernetes.handler.exit-code=1
36+
* kubernetes.sigterm-handler.enabled=true
37+
* kubernetes.sigterm-handler.exit-code=0
38+
* kubernetes.sigterm-handler.termination-message-path=/dev/termination-log
39+
* kubernetes.sigterm-handler.termination-message=SIGTERM signal received...
3240
* }
3341
* </pre>
3442
*
3543
* <p>
36-
* By default, the handler is enabled, and the application terminates with an exit code of 0,
37-
* marking the Kubernetes Pod as "Completed."
44+
* The Sigterm Handler is primarily designed for Kubernetes
45+
* but can also be utilized in Docker or other environments requiring signal handling functionality.
3846
* </p>
3947
*
4048
* @author jeyong
41-
* @since 1.0
49+
* @since 1.2
4250
* @see SigtermHandlerConfiguration
4351
*/
4452
// @formatter:on
45-
@ConfigurationProperties(prefix = "kubernetes.handler")
53+
@ConfigurationProperties(prefix = "kubernetes.sigterm-handler")
4654
public class SigtermHandlerProperties {
4755

4856
private boolean enabled = true;
49-
5057
private int exitCode = 0;
58+
private String terminationMessagePath;
59+
private String terminationMessage = "SIGTERM signal received. Application has been terminated successfully.";
5160

5261
public boolean isEnabled() {
5362
return enabled;
@@ -64,4 +73,20 @@ public int getExitCode() {
6473
public void setExitCode(final int exitCode) {
6574
this.exitCode = exitCode;
6675
}
76+
77+
public String getTerminationMessagePath() {
78+
return terminationMessagePath;
79+
}
80+
81+
public void setTerminationMessagePath(final String terminationMessagePath) {
82+
this.terminationMessagePath = terminationMessagePath;
83+
}
84+
85+
public String getTerminationMessage() {
86+
return terminationMessage;
87+
}
88+
89+
public void setTerminationMessage(final String terminationMessage) {
90+
this.terminationMessage = terminationMessage;
91+
}
6792
}

handler/src/main/java/io/jeyong/handler/SpringContextTerminator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ public class SpringContextTerminator extends ApplicationTerminator {
88
private final ApplicationContext applicationContext;
99
private final int exitCode;
1010

11-
public SpringContextTerminator(final ApplicationContext applicationContext, final int exitCode) {
11+
public SpringContextTerminator(final ApplicationContext applicationContext, final int exitCode,
12+
final String terminationMessagePath, final String terminationMessage) {
13+
super(terminationMessagePath, terminationMessage);
1214
this.applicationContext = applicationContext;
1315
this.exitCode = exitCode;
1416
}

test/src/test/java/io/jeyong/test/integration/SigtermHandlerTest.java

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.nio.file.Files;
1212
import java.nio.file.Path;
1313
import org.apache.commons.io.FileUtils;
14+
import org.junit.jupiter.api.AfterAll;
1415
import org.junit.jupiter.api.BeforeAll;
1516
import org.junit.jupiter.api.DisplayName;
1617
import org.junit.jupiter.api.Test;
@@ -22,15 +23,26 @@
2223
@DisplayName("SigtermHandler Integration Test")
2324
public class SigtermHandlerTest {
2425

26+
private static final int EXPECTED_EXIT_CODE = 10;
27+
private static final String TERMINATION_MESSAGE_PATH = "/app/termination-message.message";
28+
private static final String TERMINATION_MESSAGE = "Test termination message";
29+
30+
private static Path codeDir;
2531
private static ImageFromDockerfile dockerImage;
2632

2733
@BeforeAll
28-
static void setUp() {
29-
dockerImage = buildImage();
34+
static void setUp() throws Exception {
35+
codeDir = createCodeDirectory();
36+
dockerImage = buildImage(codeDir);
37+
}
38+
39+
@AfterAll
40+
static void tearDown() throws Exception {
41+
FileUtils.deleteDirectory(codeDir.toFile());
3042
}
3143

3244
@Test
33-
@DisplayName("Container should exit with code 0 on SIGTERM")
45+
@DisplayName("Container should exit with expected code on SIGTERM")
3446
void testExitCode() throws Exception {
3547
// given
3648
GenericContainer<?> container = new GenericContainer<>(dockerImage)
@@ -42,7 +54,7 @@ void testExitCode() throws Exception {
4254

4355
// then
4456
Long exitCode = container.getCurrentContainerInfo().getState().getExitCodeLong();
45-
assertThat(exitCode).isEqualTo(0);
57+
assertThat(exitCode).isEqualTo(EXPECTED_EXIT_CODE);
4658
}
4759

4860
@Test
@@ -65,10 +77,52 @@ void testCleanUp() throws Exception {
6577
});
6678
}
6779

68-
private static ImageFromDockerfile buildImage() {
69-
Path tempDir = createTempDirectory();
80+
@Test
81+
@DisplayName("Check termination message file inside container")
82+
void testTerminationMessageFile() throws Exception {
83+
// given
84+
GenericContainer<?> container = new GenericContainer<>(dockerImage)
85+
.waitingFor(forLogMessage(".*Started TestApplication.*\\n", 1));
86+
container.start();
87+
88+
// when
89+
sendSigtermToContainer(container);
90+
91+
// then
92+
String fileContent = container.copyFileFromContainer(
93+
TERMINATION_MESSAGE_PATH,
94+
content -> new String(content.readAllBytes())
95+
);
96+
assertThat(fileContent).isEqualTo(TERMINATION_MESSAGE);
97+
}
98+
99+
private static Path createCodeDirectory() throws Exception {
100+
Path codeDir = Files.createTempDirectory("docker-context");
101+
copyFile("../gradlew", codeDir.resolve("gradlew"));
102+
copyDirectory("../gradle", codeDir.resolve("gradle"));
103+
copyFile("../settings.gradle", codeDir.resolve("settings.gradle"));
104+
copyDirectory("../handler", codeDir.resolve("handler"));
105+
copyDirectory("../test", codeDir.resolve("test"));
106+
createApplicationYaml(codeDir.resolve("test/src/main/resources"));
107+
return codeDir;
108+
}
109+
110+
private static void createApplicationYaml(Path resourcesDir) throws Exception {
111+
Files.createDirectories(resourcesDir);
112+
Path applicationYaml = resourcesDir.resolve("application.yml");
113+
String yamlContent = String.format("""
114+
kubernetes:
115+
sigterm-handler:
116+
exit-code: %d
117+
termination-message-path: %s
118+
termination-message: %s
119+
""", EXPECTED_EXIT_CODE, TERMINATION_MESSAGE_PATH, TERMINATION_MESSAGE);
120+
Files.writeString(applicationYaml, yamlContent);
121+
}
122+
123+
private static ImageFromDockerfile buildImage(Path sourceCodeDir) {
70124
return new ImageFromDockerfile()
71-
.withFileFromPath(".", tempDir)
125+
.withFileFromPath(".", sourceCodeDir)
72126
.withDockerfileFromBuilder(builder -> {
73127
builder.from("eclipse-temurin:17")
74128
.workDir("/app")
@@ -84,20 +138,6 @@ private static ImageFromDockerfile buildImage() {
84138
});
85139
}
86140

87-
private static Path createTempDirectory() {
88-
try {
89-
Path tempDir = Files.createTempDirectory("docker-context");
90-
copyFile("../gradlew", tempDir.resolve("gradlew"));
91-
copyDirectory("../gradle", tempDir.resolve("gradle"));
92-
copyFile("../settings.gradle", tempDir.resolve("settings.gradle"));
93-
copyDirectory("../handler", tempDir.resolve("handler"));
94-
copyDirectory("../test", tempDir.resolve("test"));
95-
return tempDir;
96-
} catch (Exception e) {
97-
throw new RuntimeException("Failed to create temp directory for Docker context", e);
98-
}
99-
}
100-
101141
private static void copyFile(String source, Path destination) throws Exception {
102142
FileUtils.copyFile(new File(source), destination.toFile());
103143
}

test/src/test/java/io/jeyong/test/unit/ApplicationTerminatorTest.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,48 @@
44
import static org.assertj.core.api.Assertions.assertThat;
55

66
import io.jeyong.handler.ApplicationTerminator;
7+
import java.io.File;
8+
import java.nio.file.Files;
79
import org.junit.jupiter.api.DisplayName;
810
import org.junit.jupiter.api.Test;
911
import sun.misc.SignalHandler;
1012

1113
@DisplayName("ApplicationTerminator Unit Test")
1214
public class ApplicationTerminatorTest {
1315

16+
@Test
17+
@DisplayName("FileUtils.writeToFile should create a file with correct content")
18+
void testFileCreationAndContent() throws Exception {
19+
// given
20+
File tempFile = Files.createTempFile("termination-", ".txt").toFile();
21+
String terminationMessagePath = tempFile.getAbsolutePath();
22+
String expectedMessage = "Test termination message";
23+
24+
ApplicationTerminator terminator = new ApplicationTerminator(terminationMessagePath, expectedMessage) {
25+
26+
@Override
27+
protected int getExitCode() {
28+
return 0;
29+
}
30+
};
31+
32+
// when
33+
catchSystemExit(() -> terminator.handleTermination().handle(null));
34+
35+
// then
36+
assertThat(Files.readString(tempFile.toPath())).isEqualTo(expectedMessage);
37+
38+
// Clean up
39+
Files.deleteIfExists(tempFile.toPath());
40+
}
41+
1442
@Test
1543
@DisplayName("System.exit should be called with the expected exit code")
1644
void testHandleTerminationCallsSystemExit() throws Exception {
1745
// given
1846
int expectedExitCode = 0;
19-
ApplicationTerminator terminator = new ApplicationTerminator() {
20-
47+
ApplicationTerminator terminator = new ApplicationTerminator(null, null) {
48+
2149
@Override
2250
protected int getExitCode() {
2351
return expectedExitCode;

test/src/test/java/io/jeyong/test/unit/SignalHandlerRegistrarTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ void testSignalHandlerRegistration() {
1919
String signalType = "TERM";
2020
int exitCode = 0;
2121
SignalHandler expectedHandler = signal -> System.exit(exitCode);
22-
ApplicationTerminator terminator = new ApplicationTerminator() {
22+
ApplicationTerminator terminator = new ApplicationTerminator(null, null) {
2323

2424
@Override
2525
public SignalHandler handleTermination() {

0 commit comments

Comments
 (0)