diff --git a/README.md b/README.md index 7198d8b..cba4490 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,10 @@ Certain actions can only be performed on a container if the container is in a ce * `stop`: (Default if no pre- or post-action is set) The container will be stopped before the backup is performed. (Ignored if container is already stopped.) * `pause`: The container will be paused before the backup is performed. (Ignored if container is already paused or stopped.) * `salvage.command.pre` and `salvage.command.post`: Commands that will be executed before and after the backup within the container, similar to `docker exec`. Will not be executed if the container is stopped or paused. +* `salvage.command.exitcode`: Defines how different exit codes should be handled. Possible values are: + * `ignore`: The exit code will be ignored. + * `stop`: The backup will not be performed. (Default) + * `custom`: Special handlingg. Instead of using the `custom` value, you are expected to provide a comma-separated list of exit codes that should be handled as `stop`. You can define ranges or single exit codes. For example `1,3-5,7-9`. * `salvage.user`: User that will be used to execute the backup command. (Default is container's user) # salvage Crane Interface diff --git a/src/main/java/de/chrisliebaer/salvage/StateTransaction.java b/src/main/java/de/chrisliebaer/salvage/StateTransaction.java index d45b207..ca1653d 100644 --- a/src/main/java/de/chrisliebaer/salvage/StateTransaction.java +++ b/src/main/java/de/chrisliebaer/salvage/StateTransaction.java @@ -76,12 +76,19 @@ public void prepare(SalvageContainer container) throws InterruptedException { if (container.commandPre().isPresent() && state.getRunning() && !state.getPaused()) { var command = container.commandPre().get(); log.debug("running preperation command '{}' on container {}", command, container.name()); + long exitCode; try { - command.run(docker, container); + exitCode = command.run(docker, container); } catch (Throwable e) { throw new IllegalStateException("preperation command '" + command + "' failed on container '" + container.name() + "'", e); } + if (container.exitCodeBehaviour().check(exitCode)) { + log.debug("preperation command '{}' on container {} exited with code {}", command, container.name(), exitCode); + } else { + throw new IllegalStateException("preperation command '" + command + "' on container '" + container.name() + "' exited with code " + exitCode); + } + preCommandRun = true; } @@ -133,7 +140,13 @@ public void restore(SalvageContainer container) throws Throwable { if (affected.preCommandRun() && container.commandPost().isPresent()) { var command = container.commandPost().get(); log.debug("running post command '{}' on container {}", command, container.name()); - command.run(docker, container); + var exitCode = command.run(docker, container); + if (container.exitCodeBehaviour().check(exitCode)) { + log.debug("post command '{}' on container {} exited with code {}", command, container.name(), exitCode); + } else { + throw new IllegalStateException("post command '" + command + "' on container '" + container.name() + "' exited with code " + exitCode); + + } } } diff --git a/src/main/java/de/chrisliebaer/salvage/entity/ContainerCommand.java b/src/main/java/de/chrisliebaer/salvage/entity/ContainerCommand.java index 5f54eec..95746c3 100644 --- a/src/main/java/de/chrisliebaer/salvage/entity/ContainerCommand.java +++ b/src/main/java/de/chrisliebaer/salvage/entity/ContainerCommand.java @@ -44,8 +44,6 @@ public long run(DockerClient client, SalvageContainer container) throws Throwabl var exitCode = execInspect.getExitCodeLong(); if (exitCode == null) throw new IllegalStateException("execution of command in container " + container.name() + " failed"); - if (exitCode != 0) - log.warn("execution of command in container {} failed with exit code {}", container.name(), exitCode); return exitCode; } diff --git a/src/main/java/de/chrisliebaer/salvage/entity/ExitCodeBehaviour.java b/src/main/java/de/chrisliebaer/salvage/entity/ExitCodeBehaviour.java new file mode 100644 index 0000000..25cdc20 --- /dev/null +++ b/src/main/java/de/chrisliebaer/salvage/entity/ExitCodeBehaviour.java @@ -0,0 +1,136 @@ +package de.chrisliebaer.salvage.entity; + +import com.google.common.collect.Range; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + + +/** + * Represents the behaviour of a command in response to its exit code. + * + *

+ * The interface defines a single method, check, which takes a long value representing an exit code and returns a boolean. + */ +public sealed interface ExitCodeBehaviour permits ExitCodeBehaviour.Ignore, ExitCodeBehaviour.FailIfNonZero, ExitCodeBehaviour.Custom { + + static ExitCodeBehaviour fromString(String value) { + // first check for built-in behaviours, if none matches, parse custom behaviour, if that fails, throw exception1 + + if ("ignore".equals(value)) { + return new Ignore(); + } + if ("fail".equals(value)) { + return new FailIfNonZero(); + } + return Custom.fromString(value); + } + + boolean check(long exitCode); + + /** + * Describes behaviour which ignores the exit code of the command and continues with the backup regardless. + */ + record Ignore() implements ExitCodeBehaviour { + + @Override + public boolean check(long exitCode) { + return true; + } + } + + /** + * Describes behaviour which stops the backup if the command exits with a non-zero exit code. + */ + record FailIfNonZero() implements ExitCodeBehaviour { + + @Override + public boolean check(long exitCode) { + return exitCode == 0; + } + } + + /** + * Describes behaviour which reacts to the exit code in a custom way. + */ + record Custom(List> ranges) implements ExitCodeBehaviour { + + /** + * This pattern matches a single number or a range of numbers separated by a hyphen. Each number can be prefixed with a minus sign to indicate a negative number. + *

Examples of valid ranges:

+ * + */ + private static final Pattern RANGE_PATTERN = Pattern.compile("(?-?\\d+)-(?-?\\d+)|(?-?\\d+)"); + + /** + * Parses a string representation of the custom exit code behaviour. + * + *

+ * The string is expected to be a comma-separated list of ranges, where each range is either a single number or a range of numbers separated by a hyphen. + *

+ * + * @param str the string representation. + */ + public static Custom fromString(String str) { + // remove whitespace, allow pesky humans to add spaces + var value = str.replaceAll("\\s", ""); + var parts = value.split(","); + + var ranges = new ArrayList>(); + for (var part : parts) { + var matcher = RANGE_PATTERN.matcher(part); + + if (!matcher.matches()) { + throw new IllegalArgumentException("invalid range: " + part); + } + + var start = matcher.group("start"); + var end = matcher.group("end"); + if (start == null) { + var number = parseNumber(matcher.group("single")); + ranges.add(Range.singleton(number)); + } else { + var startNumber = parseNumber(start); + var endNumber = parseNumber(end); + + // swap if start is greater than end + if (startNumber > endNumber) { + var temp = startNumber; + startNumber = endNumber; + endNumber = temp; + } + + ranges.add(Range.closed(startNumber, endNumber)); + } + } + return new Custom(ranges); + } + + private static long parseNumber(String str) { + var isNegative = str.startsWith("-"); + var value = str; + if (isNegative) { + value = str.substring(1); + } + var number = Long.parseLong(value); + return isNegative ? -number : number; + } + + @Override + public boolean check(long exitCode) { + return ranges.stream().anyMatch(r -> r.contains(exitCode)); + } + } +} diff --git a/src/main/java/de/chrisliebaer/salvage/entity/SalvageContainer.java b/src/main/java/de/chrisliebaer/salvage/entity/SalvageContainer.java index dd608c0..c01db51 100644 --- a/src/main/java/de/chrisliebaer/salvage/entity/SalvageContainer.java +++ b/src/main/java/de/chrisliebaer/salvage/entity/SalvageContainer.java @@ -10,10 +10,12 @@ import java.util.Optional; public record SalvageContainer(String id, String name, Optional project, List volumes, - ContainerAction action, Optional commandPre, Optional commandPost) { + ContainerAction action, Optional commandPre, Optional commandPost, + ExitCodeBehaviour exitCodeBehaviour) { private static final String LABEL_CONTAINER_ACTION = "salvage.action"; + private static final String LABEL_CONTAINER_COMMAND_EXIT_CODE = "salvage.command.exitcode"; private static final String LABEL_CONTAINER_COMMAND_USER = "salvage.command.user"; private static final String LABEL_CONTAINER_COMMAND_PRE = "salvage.command.pre"; private static final String LABEL_CONTAINER_COMMAND_POST = "salvage.command.post"; @@ -47,6 +49,7 @@ public static ContainerAction fromString(String action) { } } + public static SalvageContainer fromContainer(InspectContainerResponse container, Map volumes) { var usedVolumes = new ArrayList(); var labels = container.getConfig().getLabels(); @@ -63,6 +66,10 @@ public static SalvageContainer fromContainer(InspectContainerResponse container, var postCommand = Optional.ofNullable(labels.get(LABEL_CONTAINER_COMMAND_POST)) .map(s -> new ContainerCommand(List.of(translateCommandline(s)), user)); + // parse exit code behaviour, if present + var exitCodeBehaviour = Optional.ofNullable(labels.get(LABEL_CONTAINER_COMMAND_EXIT_CODE)) + .map(ExitCodeBehaviour::fromString).orElse(new ExitCodeBehaviour.FailIfNonZero()); + // set default action depending on whether pre- or post-commands are present var action = preCommand.isPresent() || postCommand.isPresent() ? ContainerAction.IGNORE : ContainerAction.STOP; @@ -76,7 +83,7 @@ public static SalvageContainer fromContainer(InspectContainerResponse container, usedVolumes.add(volume); } - return new SalvageContainer(container.getId(), container.getName(), project, usedVolumes, action, preCommand, postCommand); + return new SalvageContainer(container.getId(), container.getName(), project, usedVolumes, action, preCommand, postCommand, exitCodeBehaviour); } private static String[] translateCommandline(String command) {