Files
matrix/cygnus-installer/src/main/java/com/cygnus/installer/DeploymentWriter.java

286 lines
12 KiB
Java

package com.cygnus.installer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Map;
import java.util.UUID;
import org.springframework.stereotype.Service;
@Service
public class DeploymentWriter {
public Path write(
Path output,
UUID installationUuid,
String installationCode,
String clientId,
ActivationDtos.ValidationResponse validation,
ActivationDtos.RegistrationResponse registration,
InstallationKeyPair keys,
String machineAssertion,
InstallerSettings settings,
ProductProfile profile,
String dockerImage,
Map<String, String> optionalFields,
RuntimeConfiguration runtime) {
try {
Path normalizedOutput = output.toAbsolutePath().normalize();
Path config = normalizedOutput.resolve("config");
protectCloudConfiguration(config, profile);
Path keyDirectory = config.resolve("keys");
Files.createDirectories(keyDirectory);
String privateRelative = "config/keys/client-signing-private.pem";
String publicRelative = "config/keys/client-signing-public.pem";
String assertionRelative = "config/machine-assertion.jwt";
writePrivate(
normalizedOutput.resolve(privateRelative),
keys.privateKeyPem());
writePublic(
normalizedOutput.resolve(publicRelative),
keys.publicKeyPem());
String loginPublicRelative = "config/keys/login-public.pem";
writePublic(
normalizedOutput.resolve(loginPublicRelative),
Files.readString(profile.loginEncryptionPublicKey()));
writePrivate(
normalizedOutput.resolve(assertionRelative),
machineAssertion);
writePublic(
config.resolve(profile.configurationFileName()),
installationConfiguration(
installationUuid,
installationCode,
clientId,
validation,
registration,
settings,
profile,
privateRelative,
publicRelative,
assertionRelative,
optionalFields,
runtime));
writePublic(
normalizedOutput.resolve("compose.yml"),
compose(profile));
writePrivate(
normalizedOutput.resolve(".env"),
environment(
profile,
dockerImage,
clientId,
installationCode,
settings,
runtime));
return normalizedOutput;
} catch (IOException exception) {
throw new IllegalStateException("Could not write installation files", exception);
}
}
private void protectCloudConfiguration(Path deploymentConfig, ProductProfile profile) {
if (isInside(deploymentConfig, profile.assertionEncryptionPublicKey())
|| isInside(deploymentConfig, profile.loginEncryptionPublicKey())) {
throw new IllegalArgumentException(
"Refusing to write deployment files over the cloud-service "
+ "configuration directory");
}
}
private boolean isInside(Path directory, Path file) {
if (file == null) {
return false;
}
return file.toAbsolutePath().normalize().startsWith(
directory.toAbsolutePath().normalize());
}
private String installationConfiguration(
UUID installationUuid,
String installationCode,
String clientId,
ActivationDtos.ValidationResponse validation,
ActivationDtos.RegistrationResponse registration,
InstallerSettings settings,
ProductProfile profile,
String privateKey,
String publicKey,
String assertion,
Map<String, String> optionalFields,
RuntimeConfiguration runtime) {
String cloudServiceUrl = normalizedCloudServiceUrl(runtime);
StringBuilder yaml = new StringBuilder()
.append(profile.configurationRoot()).append(":\n")
.append(" product: ").append(quoted(profile.product())).append('\n')
.append(" cloud-url: ")
.append(quoted(cloudServiceUrl)).append('\n')
.append(" token-url: ")
.append(quoted(cloudServiceUrl + profile.tokenPath())).append('\n')
.append(" tenant-id: ").append(quoted(value(validation.tenantId()))).append('\n')
.append(" client-id: ").append(quoted(clientId)).append('\n')
.append(" installation-id: ")
.append(quoted(value(registration.installationId()))).append('\n')
.append(" installation-uuid: ")
.append(quoted(installationUuid.toString())).append('\n')
.append(" installation-code: ").append(quoted(installationCode)).append('\n')
.append(" environment: ").append(quoted(settings.environment())).append('\n')
.append(" machine-assertion: ").append(quoted("file:./" + assertion)).append('\n')
.append(" signing-private-key: ").append(quoted("file:./" + privateKey)).append('\n')
.append(" signing-public-key: ").append(quoted("file:./" + publicKey)).append('\n');
if (!optionalFields.isEmpty()) {
yaml.append(" installation-fields:\n");
optionalFields.forEach((key, value) -> yaml
.append(" ").append(key).append(": ").append(quoted(value)).append('\n'));
}
return yaml.toString();
}
private String compose(ProductProfile profile) {
Path containerConfiguration = Path.of(profile.containerConfigurationPath());
Path containerConfigDirectory = containerConfiguration.getParent();
if (containerConfigDirectory == null) {
throw new InstallerConfigurationException(
"Profile container_configuration_path must include a directory");
}
return """
services:
%s:
image: ${%s:?Set %s}
restart: unless-stopped
ports:
- "%s"
volumes:
- ./config:%s:ro
env_file:
- ./.env
environment:
%s: %s
""".formatted(
profile.serviceName(),
profile.imageEnvironmentVariable(),
profile.imageEnvironmentVariable(),
profile.portMapping(),
containerConfigDirectory,
profile.installationConfigEnvironmentVariable(),
profile.containerConfigurationPath());
}
private String environment(
ProductProfile profile,
String dockerImage,
String clientId,
String installationCode,
InstallerSettings settings,
RuntimeConfiguration runtime) {
Path containerConfiguration = Path.of(profile.containerConfigurationPath());
Path containerConfigDirectory = containerConfiguration.getParent();
if (containerConfigDirectory == null) {
throw new InstallerConfigurationException(
"Profile container_configuration_path must include a directory");
}
String configRoot = containerConfigDirectory.toString();
StringBuilder env = new StringBuilder();
appendEnvironment(env, profile.imageEnvironmentVariable(), dockerImage);
appendEnvironment(env, "MATRIX_DB_URL", runtime.databaseUrl());
appendEnvironment(env, "MATRIX_DB_USERNAME", runtime.databaseUsername());
appendEnvironment(env, "MATRIX_DB_PASSWORD", runtime.databasePassword());
appendEnvironment(env, "REDIS_HOST", runtime.redisHost());
appendEnvironment(env, "REDIS_PORT", runtime.redisPort());
appendEnvironment(env, "REDIS_PASSWORD", runtime.redisPassword());
appendEnvironment(env, "REDIS_DATABASE", "1");
appendEnvironment(env, "REDIS_SSL", Boolean.toString(runtime.redisSsl()));
appendEnvironment(
env, "CYGNUS_CLOUD_BASE_URL", normalizedCloudServiceUrl(runtime));
appendEnvironment(
env,
"CYGNUS_TOKEN_URL",
normalizedCloudServiceUrl(runtime) + profile.tokenPath());
appendEnvironment(env, "CYGNUS_CLIENT_ID", clientId);
appendEnvironment(env, "CYGNUS_INSTALLATION_ID", installationCode);
appendEnvironment(
env,
"CYGNUS_CLIENT_ASSERTION",
"file:" + configRoot + "/machine-assertion.jwt");
appendEnvironment(env, "CYGNUS_LOGIN_KEY_ID", profile.loginKeyId());
appendEnvironment(
env,
"CYGNUS_LOGIN_PUBLIC_KEY",
"file:" + configRoot + "/keys/login-public.pem");
appendEnvironment(
env,
"CYGNUS_CLOUD_REQUEST_TIMEOUT",
runtime.cloudRequestTimeout());
appendEnvironment(
env,
"CYGNUS_TOKEN_REFRESH_SKEW",
runtime.tokenRefreshSkew());
return env.toString();
}
private String normalizedCloudServiceUrl(RuntimeConfiguration runtime) {
String value = runtime.containerCloudServiceUrl().trim();
return value.endsWith("/")
? value.substring(0, value.length() - 1)
: value;
}
private void appendEnvironment(StringBuilder env, String name, String value) {
String safeValue = value == null ? "" : value;
env.append(name)
.append("=\"")
.append(safeValue
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\r", "")
.replace("\n", ""))
.append("\"\n");
}
private String quoted(String value) {
if (value == null) {
return "\"\"";
}
return '"' + value
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\r", "")
.replace("\n", "\\n") + '"';
}
private String value(Object value) {
return value == null ? "" : value.toString();
}
private void writePrivate(Path path, String content) throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(
path,
content,
StandardCharsets.US_ASCII,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
try {
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rw-------"));
} catch (UnsupportedOperationException ignored) {
// Non-POSIX platforms use native ACLs.
}
}
private void writePublic(Path path, String content) throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(
path,
content,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
}
}