All checks were successful
Build, test and publish / verify (push) Successful in 8m54s
126 lines
5.0 KiB
Java
126 lines
5.0 KiB
Java
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.attribute.PosixFilePermissions;
|
|
import java.time.Duration;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/** Argument-safe SSH adapter; server-side mkdir lock is shared by every runner. */
|
|
class ReleaseRemote implements AutoCloseable {
|
|
final Path root, credentials;
|
|
final Map<String, String> env;
|
|
final Commands commands;
|
|
final String owner, directory, host, user, port, script;
|
|
|
|
ReleaseRemote(Path root, Map<String, String> env, Commands commands, String owner) throws Exception {
|
|
this.root = root;
|
|
this.env = env;
|
|
this.commands = commands;
|
|
this.owner = owner;
|
|
directory = Setup.required(env, "DEPLOY_DIRECTORY");
|
|
host = Setup.required(env, "DEPLOY_HOST");
|
|
user = Setup.required(env, "DEPLOY_USER");
|
|
port = env.getOrDefault("DEPLOY_SSH_PORT", "22");
|
|
Pipeline.check(
|
|
directory.matches("/[a-zA-Z0-9_./-]+") && !directory.contains("..") && !directory.equals("/"),
|
|
"Use an absolute dedicated deployment directory without spaces or parent traversal");
|
|
Pipeline.check(
|
|
host.matches("[a-zA-Z0-9][a-zA-Z0-9.-]*")
|
|
&& user.matches("[a-zA-Z0-9_][a-zA-Z0-9_-]*")
|
|
&& port.matches("[0-9]{1,5}")
|
|
&& Integer.parseInt(port) > 0
|
|
&& Integer.parseInt(port) <= 65535,
|
|
"Invalid deployment SSH host, user or port");
|
|
script = "COMPOSE_PROJECT=" + quote(Setup.required(env, "COMPOSE_PROJECT")) + "\n"
|
|
+ "DEPLOY_ROLES=" + quote(String.join(" ", Recipe.roles(env))) + "\n"
|
|
+ "DEPENDENCY_SERVICES=" + quote(env.getOrDefault("DEPENDENCY_SERVICES", "")) + "\n"
|
|
+ "TARGET_PLATFORM=" + quote(env.getOrDefault("TARGET_PLATFORM", "linux/amd64")) + "\n"
|
|
+ "REQUIRED_SECRETS=" + quote(env.getOrDefault("REQUIRED_DEPLOY_SECRETS", "")) + "\n"
|
|
+ Files.readString(root.resolve("deploy/health.sh")) + "\n"
|
|
+ Files.readString(root.resolve("generated/deploy/remote.sh"));
|
|
credentials = Files.createTempDirectory("engineering-deploy-ssh-");
|
|
try {
|
|
writePrivate("identity", Setup.required(env, "DEPLOY_SSH_KEY") + "\n");
|
|
writePrivate("known_hosts", Setup.required(env, "DEPLOY_KNOWN_HOSTS") + "\n");
|
|
} catch (Exception e) {
|
|
close();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
void writePrivate(String name, String value) throws Exception {
|
|
Path file = credentials.resolve(name);
|
|
Files.createFile(file, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
|
|
Files.writeString(file, value);
|
|
}
|
|
|
|
static String quote(String value) {
|
|
return "'" + value.replace("'", "'\"'\"'") + "'";
|
|
}
|
|
|
|
String call(String action, String input, String... arguments) throws Exception {
|
|
int seconds = switch (action) {
|
|
case "apply" -> 300;
|
|
case "prepare", "ensure" -> 600;
|
|
default -> 60;
|
|
};
|
|
var parameters = new ArrayList<>(List.of(action, directory, owner));
|
|
parameters.addAll(List.of(arguments));
|
|
return executeScript(script, input, seconds, parameters);
|
|
}
|
|
|
|
String executeScript(String source, String input, int seconds, List<String> parameters) throws Exception {
|
|
String remote = "timeout -k 5 " + seconds + " sh -c " + quote(source) + " --";
|
|
for (String argument : parameters) remote += " " + quote(argument);
|
|
var args = new ArrayList<>(List.of(
|
|
"ssh",
|
|
"-F",
|
|
"/dev/null",
|
|
"-T",
|
|
"-o",
|
|
"BatchMode=yes",
|
|
"-o",
|
|
"StrictHostKeyChecking=yes",
|
|
"-o",
|
|
"IdentitiesOnly=yes",
|
|
"-o",
|
|
"IdentityAgent=none",
|
|
"-o",
|
|
"ForwardAgent=no",
|
|
"-o",
|
|
"ConnectTimeout=10",
|
|
"-o",
|
|
"ServerAliveInterval=10",
|
|
"-o",
|
|
"ServerAliveCountMax=3",
|
|
"-o",
|
|
"LogLevel=ERROR",
|
|
"-o",
|
|
"UserKnownHostsFile=" + credentials.resolve("known_hosts"),
|
|
"-o",
|
|
"GlobalKnownHostsFile=/dev/null",
|
|
"-i",
|
|
credentials.resolve("identity").toString(),
|
|
"-p",
|
|
port,
|
|
user + "@" + host,
|
|
remote));
|
|
commands.renew(Duration.ofSeconds(seconds + 30));
|
|
return commands.execute(args, root, env, input, false).checked();
|
|
}
|
|
|
|
void acquire() throws Exception {
|
|
call("lock", null);
|
|
}
|
|
|
|
void release() throws Exception {
|
|
call("unlock", null);
|
|
}
|
|
|
|
@Override
|
|
public void close() throws Exception {
|
|
Pipeline.deleteTree(credentials);
|
|
}
|
|
}
|