S19: consume versioned engineering resources and move landscape documentation
All checks were successful
Build, test and publish / verify (push) Successful in 8m54s
All checks were successful
Build, test and publish / verify (push) Successful in 8m54s
This commit is contained in:
125
generated/ci/ReleaseRemote.java
Normal file
125
generated/ci/ReleaseRemote.java
Normal file
@@ -0,0 +1,125 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user