Skip to content

Commit

Permalink
Merge pull request #7 from joon6093/1.2.x
Browse files Browse the repository at this point in the history
Release v1.2.1
  • Loading branch information
joon6093 authored Nov 29, 2024
2 parents 69ec5e6 + f3bc425 commit d96c899
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 51 deletions.
2 changes: 1 addition & 1 deletion handler/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = 'io.jeyong'
version = '1.2.0'
version = '1.2.1'

ext {
artifactName = 'k8s-sigterm-handler'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@

public abstract class ApplicationTerminator {

private final String terminationMessagePath;
private final String terminationMessage;

protected ApplicationTerminator(final String terminationMessagePath, final String terminationMessage) {
this.terminationMessagePath = terminationMessagePath;
this.terminationMessage = terminationMessage;
}

public SignalHandler handleTermination() {
return signal -> System.exit(getExitCode());
return signal -> {
FileUtils.writeToFile(terminationMessagePath, terminationMessage);
System.exit(getExitCode());
};
}

protected abstract int getExitCode();
Expand Down
38 changes: 38 additions & 0 deletions handler/src/main/java/io/jeyong/handler/FileUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.jeyong.handler;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FileUtils {

private static final Logger logger = LoggerFactory.getLogger(FileUtils.class);

private FileUtils() {
}

public static void writeToFile(final String filePath, final String message) {
if (filePath == null || filePath.isBlank()) {
return;
}

final File file = new File(filePath);
try {
createParentDirectories(file);
try (FileWriter writer = new FileWriter(file)) {
writer.write(message);
}
} catch (IOException e) {
logger.error("Failed to write to file {}: {}", filePath, e.getMessage());
}
}

private static void createParentDirectories(final File file) throws IOException {
final File parentDir = file.getParentFile();
if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) {
throw new IOException("Failed to create parent directories for " + file.getAbsolutePath());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Configuration
@ConditionalOnProperty(
prefix = "kubernetes.handler",
prefix = "kubernetes.sigterm-handler",
name = "enabled",
havingValue = "true",
matchIfMissing = true
Expand All @@ -26,7 +26,8 @@ public SigtermHandlerConfiguration(final SigtermHandlerProperties sigtermHandler

@Bean
public ApplicationTerminator applicationTerminator(final ApplicationContext applicationContext) {
return new SpringContextTerminator(applicationContext, sigtermHandlerProperties.getExitCode());
return new SpringContextTerminator(applicationContext, sigtermHandlerProperties.getExitCode(),
sigtermHandlerProperties.getTerminationMessagePath(), sigtermHandlerProperties.getTerminationMessage());
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,62 @@
package io.jeyong.handler;

import org.springframework.boot.context.properties.ConfigurationProperties;

// @formatter:off
import org.springframework.boot.context.properties.ConfigurationProperties; /**
/**
* Configuration properties for Kubernetes SIGTERM handling.
*
* <p>
* Allows customization of the SIGTERM handler's behavior, including whether it is enabled and the exit code to use
* during graceful termination.
* Allows customization of the Sigterm Handler's behavior, including whether it is enabled,
* the exit code to use during graceful termination, and optional termination message settings.
* </p>
*
* <ul>
* <li><b>kubernetes.handler.enabled:</b> Set whether the handler is enabled or disabled (default: true).</li>
* <li><b>kubernetes.handler.exit-code:</b> Sets the exit code for graceful application termination (default: 0).</li>
* <li><b>kubernetes.sigterm-handler.enabled:</b> Set whether the handler is enabled or disabled. (default: true)</li>
* <li><b>kubernetes.sigterm-handler.exit-code:</b> Set the exit code for graceful application termination. (default: 0)</li>
* <li><b>kubernetes.sigterm-handler.termination-message-path:</b> Set the file path where the termination message should be written. (default: not set)</li>
* <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>
* </ul>
*
* <pre>
* Example configuration (YAML):
* {@code
* kubernetes:
* handler:
* sigterm-handler:
* enabled: true
* exit-code: 1
* exit-code: 0
* termination-message-path: /dev/termination-log
* termination-message: SIGTERM signal received...
* }
* </pre>
*
* <pre>
* Example configuration (Properties):
* {@code
* kubernetes.handler.enabled=true
* kubernetes.handler.exit-code=1
* kubernetes.sigterm-handler.enabled=true
* kubernetes.sigterm-handler.exit-code=0
* kubernetes.sigterm-handler.termination-message-path=/dev/termination-log
* kubernetes.sigterm-handler.termination-message=SIGTERM signal received...
* }
* </pre>
*
* <p>
* By default, the handler is enabled, and the application terminates with an exit code of 0,
* marking the Kubernetes Pod as "Completed."
* The Sigterm Handler is primarily designed for Kubernetes
* but can also be utilized in Docker or other environments requiring signal handling functionality.
* </p>
*
* @author jeyong
* @since 1.0
* @since 1.2
* @see SigtermHandlerConfiguration
*/
// @formatter:on
@ConfigurationProperties(prefix = "kubernetes.handler")
@ConfigurationProperties(prefix = "kubernetes.sigterm-handler")
public class SigtermHandlerProperties {

private boolean enabled = true;

private int exitCode = 0;
private String terminationMessagePath;
private String terminationMessage = "SIGTERM signal received. Application has been terminated successfully.";

public boolean isEnabled() {
return enabled;
Expand All @@ -64,4 +73,20 @@ public int getExitCode() {
public void setExitCode(final int exitCode) {
this.exitCode = exitCode;
}

public String getTerminationMessagePath() {
return terminationMessagePath;
}

public void setTerminationMessagePath(final String terminationMessagePath) {
this.terminationMessagePath = terminationMessagePath;
}

public String getTerminationMessage() {
return terminationMessage;
}

public void setTerminationMessage(final String terminationMessage) {
this.terminationMessage = terminationMessage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ public class SpringContextTerminator extends ApplicationTerminator {
private final ApplicationContext applicationContext;
private final int exitCode;

public SpringContextTerminator(final ApplicationContext applicationContext, final int exitCode) {
public SpringContextTerminator(final ApplicationContext applicationContext, final int exitCode,
final String terminationMessagePath, final String terminationMessage) {
super(terminationMessagePath, terminationMessage);
this.applicationContext = applicationContext;
this.exitCode = exitCode;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -22,15 +23,26 @@
@DisplayName("SigtermHandler Integration Test")
public class SigtermHandlerTest {

private static final int EXPECTED_EXIT_CODE = 10;
private static final String TERMINATION_MESSAGE_PATH = "/app/termination-message.message";
private static final String TERMINATION_MESSAGE = "Test termination message";

private static Path codeDir;
private static ImageFromDockerfile dockerImage;

@BeforeAll
static void setUp() {
dockerImage = buildImage();
static void setUp() throws Exception {
codeDir = createCodeDirectory();
dockerImage = buildImage(codeDir);
}

@AfterAll
static void tearDown() throws Exception {
FileUtils.deleteDirectory(codeDir.toFile());
}

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

// then
Long exitCode = container.getCurrentContainerInfo().getState().getExitCodeLong();
assertThat(exitCode).isEqualTo(0);
assertThat(exitCode).isEqualTo(EXPECTED_EXIT_CODE);
}

@Test
Expand All @@ -65,10 +77,52 @@ void testCleanUp() throws Exception {
});
}

private static ImageFromDockerfile buildImage() {
Path tempDir = createTempDirectory();
@Test
@DisplayName("Check termination message file inside container")
void testTerminationMessageFile() throws Exception {
// given
GenericContainer<?> container = new GenericContainer<>(dockerImage)
.waitingFor(forLogMessage(".*Started TestApplication.*\\n", 1));
container.start();

// when
sendSigtermToContainer(container);

// then
String fileContent = container.copyFileFromContainer(
TERMINATION_MESSAGE_PATH,
content -> new String(content.readAllBytes())
);
assertThat(fileContent).isEqualTo(TERMINATION_MESSAGE);
}

private static Path createCodeDirectory() throws Exception {
Path codeDir = Files.createTempDirectory("docker-context");
copyFile("../gradlew", codeDir.resolve("gradlew"));
copyDirectory("../gradle", codeDir.resolve("gradle"));
copyFile("../settings.gradle", codeDir.resolve("settings.gradle"));
copyDirectory("../handler", codeDir.resolve("handler"));
copyDirectory("../test", codeDir.resolve("test"));
createApplicationYaml(codeDir.resolve("test/src/main/resources"));
return codeDir;
}

private static void createApplicationYaml(Path resourcesDir) throws Exception {
Files.createDirectories(resourcesDir);
Path applicationYaml = resourcesDir.resolve("application.yml");
String yamlContent = String.format("""
kubernetes:
sigterm-handler:
exit-code: %d
termination-message-path: %s
termination-message: %s
""", EXPECTED_EXIT_CODE, TERMINATION_MESSAGE_PATH, TERMINATION_MESSAGE);
Files.writeString(applicationYaml, yamlContent);
}

private static ImageFromDockerfile buildImage(Path sourceCodeDir) {
return new ImageFromDockerfile()
.withFileFromPath(".", tempDir)
.withFileFromPath(".", sourceCodeDir)
.withDockerfileFromBuilder(builder -> {
builder.from("eclipse-temurin:17")
.workDir("/app")
Expand All @@ -84,20 +138,6 @@ private static ImageFromDockerfile buildImage() {
});
}

private static Path createTempDirectory() {
try {
Path tempDir = Files.createTempDirectory("docker-context");
copyFile("../gradlew", tempDir.resolve("gradlew"));
copyDirectory("../gradle", tempDir.resolve("gradle"));
copyFile("../settings.gradle", tempDir.resolve("settings.gradle"));
copyDirectory("../handler", tempDir.resolve("handler"));
copyDirectory("../test", tempDir.resolve("test"));
return tempDir;
} catch (Exception e) {
throw new RuntimeException("Failed to create temp directory for Docker context", e);
}
}

private static void copyFile(String source, Path destination) throws Exception {
FileUtils.copyFile(new File(source), destination.toFile());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,48 @@
import static org.assertj.core.api.Assertions.assertThat;

import io.jeyong.handler.ApplicationTerminator;
import java.io.File;
import java.nio.file.Files;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import sun.misc.SignalHandler;

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

@Test
@DisplayName("FileUtils.writeToFile should create a file with correct content")
void testFileCreationAndContent() throws Exception {
// given
File tempFile = Files.createTempFile("termination-", ".txt").toFile();
String terminationMessagePath = tempFile.getAbsolutePath();
String expectedMessage = "Test termination message";

ApplicationTerminator terminator = new ApplicationTerminator(terminationMessagePath, expectedMessage) {

@Override
protected int getExitCode() {
return 0;
}
};

// when
catchSystemExit(() -> terminator.handleTermination().handle(null));

// then
assertThat(Files.readString(tempFile.toPath())).isEqualTo(expectedMessage);

// Clean up
Files.deleteIfExists(tempFile.toPath());
}

@Test
@DisplayName("System.exit should be called with the expected exit code")
void testHandleTerminationCallsSystemExit() throws Exception {
// given
int expectedExitCode = 0;
ApplicationTerminator terminator = new ApplicationTerminator() {
ApplicationTerminator terminator = new ApplicationTerminator(null, null) {

@Override
protected int getExitCode() {
return expectedExitCode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ void testSignalHandlerRegistration() {
String signalType = "TERM";
int exitCode = 0;
SignalHandler expectedHandler = signal -> System.exit(exitCode);
ApplicationTerminator terminator = new ApplicationTerminator() {
ApplicationTerminator terminator = new ApplicationTerminator(null, null) {

@Override
public SignalHandler handleTermination() {
Expand Down
Loading

0 comments on commit d96c899

Please sign in to comment.