Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v1.2.1 #7

Merged
merged 12 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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