Skip to content

Commit

Permalink
✨ Allow configuration of expected exit codes from post and pre comman…
Browse files Browse the repository at this point in the history
…ds (#11)
  • Loading branch information
chrisliebaer authored Feb 21, 2024
1 parent 44443dc commit fe6b731
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 6 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions src/main/java/de/chrisliebaer/salvage/StateTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);

}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
136 changes: 136 additions & 0 deletions src/main/java/de/chrisliebaer/salvage/entity/ExitCodeBehaviour.java
Original file line number Diff line number Diff line change
@@ -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.
* <ul>
* <li>The Ignore record represents a behaviour that ignores the exit code of a command and continues with the backup regardless.</li>
* <li>The FailIfNonZero record represents a behaviour that stops the backup if the command exits with a non-zero exit code.</li>
* <li>The Custom record represents a behaviour that reacts to the exit code in a custom way, defined by a list of ranges.</li>
* </ul>
* <p>
* 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<Range<Long>> 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.
* <p>Examples of valid ranges:</p>
* <ul>
* <li>1-5</li>
* <li>-5--1</li>
* <li>1</li>
* <li>-1</li>
* <li>1--1</li>
* <li>-2-1</li>
* </ul>
*/
private static final Pattern RANGE_PATTERN = Pattern.compile("(?<start>-?\\d+)-(?<end>-?\\d+)|(?<single>-?\\d+)");

/**
* Parses a string representation of the custom exit code behaviour.
*
* <p>
* 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.
* </p>
*
* @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<Range<Long>>();
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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import java.util.Optional;

public record SalvageContainer(String id, String name, Optional<String> project, List<SalvageVolume> volumes,
ContainerAction action, Optional<ContainerCommand> commandPre, Optional<ContainerCommand> commandPost) {
ContainerAction action, Optional<ContainerCommand> commandPre, Optional<ContainerCommand> 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";
Expand Down Expand Up @@ -47,6 +49,7 @@ public static ContainerAction fromString(String action) {
}
}


public static SalvageContainer fromContainer(InspectContainerResponse container, Map<String, SalvageVolume> volumes) {
var usedVolumes = new ArrayList<SalvageVolume>();
var labels = container.getConfig().getLabels();
Expand All @@ -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;

Expand All @@ -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) {
Expand Down

0 comments on commit fe6b731

Please sign in to comment.