Docker installer workflow and licensing flow done - docker container is working fine
This commit is contained in:
@@ -21,9 +21,12 @@ INI. Both profiles use these cloud endpoints by default:
|
|||||||
- `POST /api/v1/installations/register`
|
- `POST /api/v1/installations/register`
|
||||||
|
|
||||||
The desktop wizard is the default. It checks Docker, collects activation and
|
The desktop wizard is the default. It checks Docker, collects activation and
|
||||||
installation details, lets the user choose an output directory, and reports
|
installation details, asks for the Docker registry, image, username, and
|
||||||
installation progress. For servers without a desktop or for automation, append
|
password, lets the user choose an output directory, and reports installation
|
||||||
`--cli` to retain the interactive terminal workflow:
|
progress. It authenticates with `docker login --password-stdin`; the registry
|
||||||
|
password is never written into generated files or command arguments. For
|
||||||
|
servers without a desktop or for automation, append `--cli` to retain the
|
||||||
|
interactive terminal workflow:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
TECHNOBEE_INSTALLER_CONFIG=cygnus-installer/config/matrix-installer.ini \
|
TECHNOBEE_INSTALLER_CONFIG=cygnus-installer/config/matrix-installer.ini \
|
||||||
@@ -48,6 +51,7 @@ configuration_root=matrix
|
|||||||
configuration_file_name=installation.yml
|
configuration_file_name=installation.yml
|
||||||
installation_config_environment_variable=MATRIX_INSTALLATION_CONFIG
|
installation_config_environment_variable=MATRIX_INSTALLATION_CONFIG
|
||||||
container_configuration_path=/opt/matrix/config/installation.yml
|
container_configuration_path=/opt/matrix/config/installation.yml
|
||||||
|
port_mapping=8080:8080
|
||||||
token_path=/oauth2/token
|
token_path=/oauth2/token
|
||||||
assertion_encryption_public_key=/secure/cloud-assertion-public.pem
|
assertion_encryption_public_key=/secure/cloud-assertion-public.pem
|
||||||
optional_fields=
|
optional_fields=
|
||||||
@@ -72,7 +76,8 @@ config/
|
|||||||
The installer asks for the product Docker image, validates it, and stores it
|
The installer asks for the product Docker image, validates it, and stores it
|
||||||
under the profile's image variable in `.env`. Docker Compose loads this file
|
under the profile's image variable in `.env`. Docker Compose loads this file
|
||||||
automatically, so no separate `export MATRIX_IMAGE` or `export CYGNUS_IMAGE`
|
automatically, so no separate `export MATRIX_IMAGE` or `export CYGNUS_IMAGE`
|
||||||
step is required.
|
step is required. Docker stores successful login state using its configured
|
||||||
|
credential store; the installer does not persist the registry password.
|
||||||
|
|
||||||
The RSA-3072 pair is generated locally. Only the public key is sent during
|
The RSA-3072 pair is generated locally. Only the public key is sent during
|
||||||
registration. The private key and encrypted machine assertion remain on
|
registration. The private key and encrypted machine assertion remain on
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[installer]
|
[installer]
|
||||||
product=cygnus
|
product=cygnus
|
||||||
cloud_service_url=http://localhost:8090
|
cloud_service_url=http://localhost:8090
|
||||||
|
container_cloud_service_url=http://host.docker.internal:8090
|
||||||
installer_api_base_path=/api/v1/installations
|
installer_api_base_path=/api/v1/installations
|
||||||
environment=production
|
environment=production
|
||||||
output_directory=./cygnus-installation
|
output_directory=./cygnus-installation
|
||||||
@@ -13,6 +14,10 @@ configuration_root=cygnus
|
|||||||
configuration_file_name=installation.yml
|
configuration_file_name=installation.yml
|
||||||
installation_config_environment_variable=CYGNUS_INSTALLATION_CONFIG
|
installation_config_environment_variable=CYGNUS_INSTALLATION_CONFIG
|
||||||
container_configuration_path=/opt/cygnus/config/installation.yml
|
container_configuration_path=/opt/cygnus/config/installation.yml
|
||||||
|
port_mapping=8080:8080
|
||||||
|
service_path=/
|
||||||
token_path=/oauth2/token
|
token_path=/oauth2/token
|
||||||
assertion_encryption_public_key=../../config/keys/assertion-decryption-public.pem
|
assertion_encryption_public_key=../../config/keys/assertion-decryption-public.pem
|
||||||
|
login_encryption_public_key=../../config/keys/login-public.pem
|
||||||
|
login_key_id=cygnus-login-2026-01
|
||||||
optional_fields=
|
optional_fields=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[installer]
|
[installer]
|
||||||
product=matrix
|
product=matrix
|
||||||
cloud_service_url=http://localhost:8090
|
cloud_service_url=http://localhost:8090
|
||||||
|
container_cloud_service_url=http://host.docker.internal:8090
|
||||||
installer_api_base_path=/api/v1/installations
|
installer_api_base_path=/api/v1/installations
|
||||||
environment=production
|
environment=production
|
||||||
output_directory=./matrix-installation
|
output_directory=./matrix-installation
|
||||||
@@ -13,6 +14,10 @@ configuration_root=matrix
|
|||||||
configuration_file_name=installation.yml
|
configuration_file_name=installation.yml
|
||||||
installation_config_environment_variable=MATRIX_INSTALLATION_CONFIG
|
installation_config_environment_variable=MATRIX_INSTALLATION_CONFIG
|
||||||
container_configuration_path=/opt/matrix/config/installation.yml
|
container_configuration_path=/opt/matrix/config/installation.yml
|
||||||
|
port_mapping=8080:8080
|
||||||
|
service_path=/matrix/
|
||||||
token_path=/oauth2/token
|
token_path=/oauth2/token
|
||||||
assertion_encryption_public_key=../../config/keys/assertion-decryption-public.pem
|
assertion_encryption_public_key=../../config/keys/assertion-decryption-public.pem
|
||||||
|
login_encryption_public_key=../../config/keys/login-public.pem
|
||||||
|
login_key_id=cygnus-login-2026-01
|
||||||
optional_fields=
|
optional_fields=
|
||||||
|
|||||||
@@ -11,10 +11,25 @@ import org.springframework.stereotype.Component;
|
|||||||
public class CommandExecutor {
|
public class CommandExecutor {
|
||||||
|
|
||||||
public CommandResult execute(List<String> command, Duration timeout) {
|
public CommandResult execute(List<String> command, Duration timeout) {
|
||||||
|
return execute(command, timeout, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandResult execute(
|
||||||
|
List<String> command,
|
||||||
|
Duration timeout,
|
||||||
|
String standardInput) {
|
||||||
try {
|
try {
|
||||||
Process process = new ProcessBuilder(command)
|
Process process = new ProcessBuilder(command)
|
||||||
.redirectErrorStream(true)
|
.redirectErrorStream(true)
|
||||||
.start();
|
.start();
|
||||||
|
if (standardInput != null) {
|
||||||
|
try (var input = process.getOutputStream()) {
|
||||||
|
input.write(standardInput.getBytes(StandardCharsets.UTF_8));
|
||||||
|
input.flush();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.getOutputStream().close();
|
||||||
|
}
|
||||||
boolean completed = process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
boolean completed = process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||||
if (!completed) {
|
if (!completed) {
|
||||||
process.destroyForcibly();
|
process.destroyForcibly();
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ public class DeploymentWriter {
|
|||||||
InstallerSettings settings,
|
InstallerSettings settings,
|
||||||
ProductProfile profile,
|
ProductProfile profile,
|
||||||
String dockerImage,
|
String dockerImage,
|
||||||
Map<String, String> optionalFields) {
|
Map<String, String> optionalFields,
|
||||||
|
RuntimeConfiguration runtime) {
|
||||||
try {
|
try {
|
||||||
Path normalizedOutput = output.toAbsolutePath().normalize();
|
Path normalizedOutput = output.toAbsolutePath().normalize();
|
||||||
Path config = normalizedOutput.resolve("config");
|
Path config = normalizedOutput.resolve("config");
|
||||||
@@ -41,6 +42,10 @@ public class DeploymentWriter {
|
|||||||
writePublic(
|
writePublic(
|
||||||
normalizedOutput.resolve(publicRelative),
|
normalizedOutput.resolve(publicRelative),
|
||||||
keys.publicKeyPem());
|
keys.publicKeyPem());
|
||||||
|
String loginPublicRelative = "config/keys/login-public.pem";
|
||||||
|
writePublic(
|
||||||
|
normalizedOutput.resolve(loginPublicRelative),
|
||||||
|
Files.readString(profile.loginEncryptionPublicKey()));
|
||||||
writePrivate(
|
writePrivate(
|
||||||
normalizedOutput.resolve(assertionRelative),
|
normalizedOutput.resolve(assertionRelative),
|
||||||
machineAssertion);
|
machineAssertion);
|
||||||
@@ -57,13 +62,20 @@ public class DeploymentWriter {
|
|||||||
privateRelative,
|
privateRelative,
|
||||||
publicRelative,
|
publicRelative,
|
||||||
assertionRelative,
|
assertionRelative,
|
||||||
optionalFields));
|
optionalFields,
|
||||||
|
runtime));
|
||||||
writePublic(
|
writePublic(
|
||||||
normalizedOutput.resolve("compose.yml"),
|
normalizedOutput.resolve("compose.yml"),
|
||||||
compose(profile));
|
compose(profile));
|
||||||
writePrivate(
|
writePrivate(
|
||||||
normalizedOutput.resolve(".env"),
|
normalizedOutput.resolve(".env"),
|
||||||
profile.imageEnvironmentVariable() + "=" + dockerImage + "\n");
|
environment(
|
||||||
|
profile,
|
||||||
|
dockerImage,
|
||||||
|
clientId,
|
||||||
|
installationCode,
|
||||||
|
settings,
|
||||||
|
runtime));
|
||||||
return normalizedOutput;
|
return normalizedOutput;
|
||||||
} catch (IOException exception) {
|
} catch (IOException exception) {
|
||||||
throw new IllegalStateException("Could not write installation files", exception);
|
throw new IllegalStateException("Could not write installation files", exception);
|
||||||
@@ -81,14 +93,16 @@ public class DeploymentWriter {
|
|||||||
String privateKey,
|
String privateKey,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
String assertion,
|
String assertion,
|
||||||
Map<String, String> optionalFields) {
|
Map<String, String> optionalFields,
|
||||||
|
RuntimeConfiguration runtime) {
|
||||||
|
String cloudServiceUrl = normalizedCloudServiceUrl(runtime);
|
||||||
StringBuilder yaml = new StringBuilder()
|
StringBuilder yaml = new StringBuilder()
|
||||||
.append(profile.configurationRoot()).append(":\n")
|
.append(profile.configurationRoot()).append(":\n")
|
||||||
.append(" product: ").append(quoted(profile.product())).append('\n')
|
.append(" product: ").append(quoted(profile.product())).append('\n')
|
||||||
.append(" cloud-url: ")
|
.append(" cloud-url: ")
|
||||||
.append(quoted(settings.cloudServiceUrl().toString())).append('\n')
|
.append(quoted(cloudServiceUrl)).append('\n')
|
||||||
.append(" token-url: ")
|
.append(" token-url: ")
|
||||||
.append(quoted(settings.cloudServiceUrl() + profile.tokenPath())).append('\n')
|
.append(quoted(cloudServiceUrl + profile.tokenPath())).append('\n')
|
||||||
.append(" tenant-id: ").append(quoted(value(validation.tenantId()))).append('\n')
|
.append(" tenant-id: ").append(quoted(value(validation.tenantId()))).append('\n')
|
||||||
.append(" client-id: ").append(quoted(clientId)).append('\n')
|
.append(" client-id: ").append(quoted(clientId)).append('\n')
|
||||||
.append(" installation-id: ")
|
.append(" installation-id: ")
|
||||||
@@ -120,19 +134,94 @@ public class DeploymentWriter {
|
|||||||
%s:
|
%s:
|
||||||
image: ${%s:?Set %s}
|
image: ${%s:?Set %s}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "%s"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:%s:ro
|
- ./config:%s:ro
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
environment:
|
environment:
|
||||||
%s: %s
|
%s: %s
|
||||||
""".formatted(
|
""".formatted(
|
||||||
profile.serviceName(),
|
profile.serviceName(),
|
||||||
profile.imageEnvironmentVariable(),
|
profile.imageEnvironmentVariable(),
|
||||||
profile.imageEnvironmentVariable(),
|
profile.imageEnvironmentVariable(),
|
||||||
|
profile.portMapping(),
|
||||||
containerConfigDirectory,
|
containerConfigDirectory,
|
||||||
profile.installationConfigEnvironmentVariable(),
|
profile.installationConfigEnvironmentVariable(),
|
||||||
profile.containerConfigurationPath());
|
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_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) {
|
private String quoted(String value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "\"\"";
|
return "\"\"";
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.cygnus.installer;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DockerRegistryLoginService {
|
||||||
|
|
||||||
|
private static final Duration LOGIN_TIMEOUT = Duration.ofSeconds(45);
|
||||||
|
private final CommandExecutor executor;
|
||||||
|
|
||||||
|
public DockerRegistryLoginService(CommandExecutor executor) {
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void login(String registry, String username, String password) {
|
||||||
|
CommandResult result = executor.execute(
|
||||||
|
List.of(
|
||||||
|
"docker",
|
||||||
|
"login",
|
||||||
|
registry,
|
||||||
|
"--username",
|
||||||
|
username,
|
||||||
|
"--password-stdin"),
|
||||||
|
LOGIN_TIMEOUT,
|
||||||
|
password + System.lineSeparator());
|
||||||
|
if (!result.successful()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Docker registry login failed. " + result.output());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,18 @@ import java.nio.file.Path;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public record InstallationRequest(
|
public record InstallationRequest(
|
||||||
|
String installerCloudServiceUrl,
|
||||||
String clientCode,
|
String clientCode,
|
||||||
String licenseKey,
|
String licenseKey,
|
||||||
String installationCode,
|
String installationCode,
|
||||||
String installationName,
|
String installationName,
|
||||||
String dockerImage,
|
String dockerImage,
|
||||||
|
String dockerRegistry,
|
||||||
|
String dockerUsername,
|
||||||
|
String dockerPassword,
|
||||||
Path outputDirectory,
|
Path outputDirectory,
|
||||||
Map<String, String> optionalFields) {
|
Map<String, String> optionalFields,
|
||||||
|
RuntimeConfiguration runtimeConfiguration) {
|
||||||
|
|
||||||
public InstallationRequest {
|
public InstallationRequest {
|
||||||
optionalFields = optionalFields == null ? Map.of() : Map.copyOf(optionalFields);
|
optionalFields = optionalFields == null ? Map.of() : Map.copyOf(optionalFields);
|
||||||
|
|||||||
@@ -8,26 +8,32 @@ import org.springframework.stereotype.Service;
|
|||||||
public class InstallationService {
|
public class InstallationService {
|
||||||
|
|
||||||
private final DockerPrerequisiteChecker prerequisites;
|
private final DockerPrerequisiteChecker prerequisites;
|
||||||
|
private final DockerRegistryLoginService registryLogin;
|
||||||
private final ActivationClient activationClient;
|
private final ActivationClient activationClient;
|
||||||
private final InstallationKeyService keyService;
|
private final InstallationKeyService keyService;
|
||||||
private final MachineAssertionService assertionService;
|
private final MachineAssertionService assertionService;
|
||||||
private final DeploymentWriter deploymentWriter;
|
private final DeploymentWriter deploymentWriter;
|
||||||
|
private final DockerComposeDeploymentService composeDeployment;
|
||||||
private final InstallerSettings settings;
|
private final InstallerSettings settings;
|
||||||
private final ProductProfile profile;
|
private final ProductProfile profile;
|
||||||
|
|
||||||
public InstallationService(
|
public InstallationService(
|
||||||
DockerPrerequisiteChecker prerequisites,
|
DockerPrerequisiteChecker prerequisites,
|
||||||
|
DockerRegistryLoginService registryLogin,
|
||||||
ActivationClient activationClient,
|
ActivationClient activationClient,
|
||||||
InstallationKeyService keyService,
|
InstallationKeyService keyService,
|
||||||
MachineAssertionService assertionService,
|
MachineAssertionService assertionService,
|
||||||
DeploymentWriter deploymentWriter,
|
DeploymentWriter deploymentWriter,
|
||||||
|
DockerComposeDeploymentService composeDeployment,
|
||||||
InstallerSettings settings,
|
InstallerSettings settings,
|
||||||
ProductProfile profile) {
|
ProductProfile profile) {
|
||||||
this.prerequisites = prerequisites;
|
this.prerequisites = prerequisites;
|
||||||
|
this.registryLogin = registryLogin;
|
||||||
this.activationClient = activationClient;
|
this.activationClient = activationClient;
|
||||||
this.keyService = keyService;
|
this.keyService = keyService;
|
||||||
this.assertionService = assertionService;
|
this.assertionService = assertionService;
|
||||||
this.deploymentWriter = deploymentWriter;
|
this.deploymentWriter = deploymentWriter;
|
||||||
|
this.composeDeployment = composeDeployment;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
}
|
}
|
||||||
@@ -43,9 +49,16 @@ public class InstallationService {
|
|||||||
progress.update(5, "Checking Docker prerequisites");
|
progress.update(5, "Checking Docker prerequisites");
|
||||||
prerequisites.verify();
|
prerequisites.verify();
|
||||||
|
|
||||||
|
progress.update(12, "Signing in to Docker registry");
|
||||||
|
registryLogin.login(
|
||||||
|
request.dockerRegistry().trim(),
|
||||||
|
request.dockerUsername().trim(),
|
||||||
|
request.dockerPassword());
|
||||||
|
|
||||||
UUID installationUuid = UUID.randomUUID();
|
UUID installationUuid = UUID.randomUUID();
|
||||||
progress.update(20, "Validating activation key");
|
progress.update(20, "Validating activation key");
|
||||||
var validation = activationClient.validate(
|
var validation = activationClient.validate(
|
||||||
|
request.installerCloudServiceUrl().trim(),
|
||||||
request.clientCode().trim(),
|
request.clientCode().trim(),
|
||||||
request.licenseKey(),
|
request.licenseKey(),
|
||||||
installationUuid,
|
installationUuid,
|
||||||
@@ -57,6 +70,7 @@ public class InstallationService {
|
|||||||
progress.update(58, "Registering installation with "
|
progress.update(58, "Registering installation with "
|
||||||
+ profile.displayName() + " cloud service");
|
+ profile.displayName() + " cloud service");
|
||||||
var registration = activationClient.register(
|
var registration = activationClient.register(
|
||||||
|
request.installerCloudServiceUrl().trim(),
|
||||||
validation,
|
validation,
|
||||||
request.installationCode().trim(),
|
request.installationCode().trim(),
|
||||||
request.installationName().trim(),
|
request.installationName().trim(),
|
||||||
@@ -72,11 +86,12 @@ public class InstallationService {
|
|||||||
String assertion = assertionService.generate(
|
String assertion = assertionService.generate(
|
||||||
clientId,
|
clientId,
|
||||||
request.installationCode().trim(),
|
request.installationCode().trim(),
|
||||||
settings.cloudServiceUrl() + profile.tokenPath(),
|
normalizedCloudServiceUrl(request.installerCloudServiceUrl())
|
||||||
|
+ profile.tokenPath(),
|
||||||
keys,
|
keys,
|
||||||
profile);
|
profile);
|
||||||
|
|
||||||
progress.update(88, "Writing on-premises deployment files");
|
progress.update(82, "Writing protected on-premises deployment files");
|
||||||
Path output = deploymentWriter.write(
|
Path output = deploymentWriter.write(
|
||||||
request.outputDirectory().toAbsolutePath().normalize(),
|
request.outputDirectory().toAbsolutePath().normalize(),
|
||||||
installationUuid,
|
installationUuid,
|
||||||
@@ -89,13 +104,27 @@ public class InstallationService {
|
|||||||
settings,
|
settings,
|
||||||
profile,
|
profile,
|
||||||
request.dockerImage().trim(),
|
request.dockerImage().trim(),
|
||||||
request.optionalFields());
|
request.optionalFields(),
|
||||||
progress.update(100, "Installation package created successfully");
|
request.runtimeConfiguration());
|
||||||
|
|
||||||
|
progress.update(88, "Pulling the on-premises service image");
|
||||||
|
composeDeployment.pull(output);
|
||||||
|
progress.update(94, "Starting the on-premises service");
|
||||||
|
composeDeployment.start(output);
|
||||||
|
progress.update(98, "Verifying the service container");
|
||||||
|
composeDeployment.verifyRunning(output, profile.serviceName());
|
||||||
|
progress.update(100, "Installation completed and service is running");
|
||||||
|
|
||||||
|
String hostPort = profile.portMapping().substring(
|
||||||
|
0, profile.portMapping().indexOf(':'));
|
||||||
return new InstallationResult(
|
return new InstallationResult(
|
||||||
registration.installationId(),
|
registration.installationId(),
|
||||||
request.installationCode().trim(),
|
request.installationCode().trim(),
|
||||||
clientId,
|
clientId,
|
||||||
output);
|
output,
|
||||||
|
profile.serviceName(),
|
||||||
|
"http://localhost:" + hostPort + profile.servicePath(),
|
||||||
|
"RUNNING");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validate(InstallationRequest request) {
|
private void validate(InstallationRequest request) {
|
||||||
@@ -103,18 +132,55 @@ public class InstallationService {
|
|||||||
throw new IllegalArgumentException("Installation request is required");
|
throw new IllegalArgumentException("Installation request is required");
|
||||||
}
|
}
|
||||||
required(request.clientCode(), "Client code");
|
required(request.clientCode(), "Client code");
|
||||||
|
required(request.installerCloudServiceUrl(), "Cloud service URL");
|
||||||
|
validateAbsoluteHttpUrl(
|
||||||
|
request.installerCloudServiceUrl(),
|
||||||
|
"Cloud service URL");
|
||||||
required(request.licenseKey(), "License key");
|
required(request.licenseKey(), "License key");
|
||||||
required(request.installationCode(), "Installation code");
|
required(request.installationCode(), "Installation code");
|
||||||
required(request.installationName(), "Installation name");
|
required(request.installationName(), "Installation name");
|
||||||
required(request.dockerImage(), "Docker image");
|
required(request.dockerImage(), "Docker image");
|
||||||
|
required(request.dockerRegistry(), "Docker registry");
|
||||||
|
required(request.dockerUsername(), "Docker username");
|
||||||
|
required(request.dockerPassword(), "Docker password");
|
||||||
if (!request.dockerImage().matches(
|
if (!request.dockerImage().matches(
|
||||||
"[A-Za-z0-9][A-Za-z0-9._:/@-]{0,254}")) {
|
"[A-Za-z0-9][A-Za-z0-9._:/@-]{0,254}")) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Docker image contains invalid characters");
|
"Docker image contains invalid characters");
|
||||||
}
|
}
|
||||||
|
if (!request.dockerRegistry().matches(
|
||||||
|
"[A-Za-z0-9][A-Za-z0-9.-]*(?::[0-9]{1,5})?")) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Docker registry must be a host name with an optional port");
|
||||||
|
}
|
||||||
if (request.outputDirectory() == null) {
|
if (request.outputDirectory() == null) {
|
||||||
throw new IllegalArgumentException("Output directory is required");
|
throw new IllegalArgumentException("Output directory is required");
|
||||||
}
|
}
|
||||||
|
RuntimeConfiguration runtime = request.runtimeConfiguration();
|
||||||
|
if (runtime == null) {
|
||||||
|
throw new IllegalArgumentException("Runtime configuration is required");
|
||||||
|
}
|
||||||
|
required(runtime.containerCloudServiceUrl(), "Container cloud service URL");
|
||||||
|
validateAbsoluteHttpUrl(
|
||||||
|
runtime.containerCloudServiceUrl(),
|
||||||
|
"Container cloud service URL");
|
||||||
|
required(runtime.databaseUrl(), "Database URL");
|
||||||
|
required(runtime.databaseUsername(), "Database username");
|
||||||
|
required(runtime.databasePassword(), "Database password");
|
||||||
|
required(runtime.redisHost(), "Redis host");
|
||||||
|
required(runtime.redisPort(), "Redis port");
|
||||||
|
required(runtime.redisPassword(), "Redis password");
|
||||||
|
required(runtime.cloudRequestTimeout(), "Cloud request timeout");
|
||||||
|
required(runtime.tokenRefreshSkew(), "Token refresh skew");
|
||||||
|
try {
|
||||||
|
int redisPort = Integer.parseInt(runtime.redisPort());
|
||||||
|
if (redisPort < 1 || redisPort > 65535) {
|
||||||
|
throw new NumberFormatException();
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Redis port must be between 1 and 65535");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void required(String value, String label) {
|
private void required(String value, String label) {
|
||||||
@@ -122,4 +188,26 @@ public class InstallationService {
|
|||||||
throw new IllegalArgumentException(label + " is required");
|
throw new IllegalArgumentException(label + " is required");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateAbsoluteHttpUrl(String value, String label) {
|
||||||
|
try {
|
||||||
|
var uri = java.net.URI.create(value.trim());
|
||||||
|
if (!uri.isAbsolute()
|
||||||
|
|| (!"http".equalsIgnoreCase(uri.getScheme())
|
||||||
|
&& !"https".equalsIgnoreCase(uri.getScheme()))
|
||||||
|
|| uri.getHost() == null) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
label + " must be an absolute HTTP or HTTPS URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizedCloudServiceUrl(String value) {
|
||||||
|
String normalized = value.trim();
|
||||||
|
return normalized.endsWith("/")
|
||||||
|
? normalized.substring(0, normalized.length() - 1)
|
||||||
|
: normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ public class InstallationWizard {
|
|||||||
|
|
||||||
Console console = System.console();
|
Console console = System.console();
|
||||||
Scanner scanner = console == null ? new Scanner(System.in) : null;
|
Scanner scanner = console == null ? new Scanner(System.in) : null;
|
||||||
|
String cloudServiceUrl = promptWithDefault(
|
||||||
|
console,
|
||||||
|
scanner,
|
||||||
|
"Cloud service URL",
|
||||||
|
settings.cloudServiceUrl().toString());
|
||||||
String clientCode = prompt(console, scanner, "Client code");
|
String clientCode = prompt(console, scanner, "Client code");
|
||||||
String licenseKey = secret(console, scanner, "License key");
|
String licenseKey = secret(console, scanner, "License key");
|
||||||
String installationCode = prompt(console, scanner, "Installation code");
|
String installationCode = prompt(console, scanner, "Installation code");
|
||||||
@@ -39,7 +44,25 @@ public class InstallationWizard {
|
|||||||
console,
|
console,
|
||||||
scanner,
|
scanner,
|
||||||
profile.displayName() + " Docker image");
|
profile.displayName() + " Docker image");
|
||||||
|
String dockerRegistry = prompt(console, scanner, "Docker registry");
|
||||||
|
String dockerUsername = prompt(console, scanner, "Docker username");
|
||||||
|
String dockerPassword = secret(console, scanner, "Docker password");
|
||||||
Map<String, String> optionalFields = promptOptionalFields(console, scanner);
|
Map<String, String> optionalFields = promptOptionalFields(console, scanner);
|
||||||
|
String containerCloudServiceUrl = promptWithDefault(
|
||||||
|
console,
|
||||||
|
scanner,
|
||||||
|
"Container cloud service URL",
|
||||||
|
settings.containerCloudServiceUrl().toString());
|
||||||
|
String databaseUrl = promptWithDefault(
|
||||||
|
console, scanner, "Database URL",
|
||||||
|
"jdbc:postgresql://localhost:5432/matrix");
|
||||||
|
String databaseUsername = promptWithDefault(
|
||||||
|
console, scanner, "Database username", "postgres");
|
||||||
|
String databasePassword = secret(console, scanner, "Database password");
|
||||||
|
String redisHost = promptWithDefault(console, scanner, "Redis host", "localhost");
|
||||||
|
String redisPort = promptWithDefault(console, scanner, "Redis port", "7901");
|
||||||
|
String redisPassword = secret(console, scanner, "Redis password");
|
||||||
|
String redisSsl = promptWithDefault(console, scanner, "Redis SSL", "false");
|
||||||
String outputDirectory = promptWithDefault(
|
String outputDirectory = promptWithDefault(
|
||||||
console,
|
console,
|
||||||
scanner,
|
scanner,
|
||||||
@@ -48,16 +71,33 @@ public class InstallationWizard {
|
|||||||
|
|
||||||
InstallationResult result = installationService.install(
|
InstallationResult result = installationService.install(
|
||||||
new InstallationRequest(
|
new InstallationRequest(
|
||||||
|
cloudServiceUrl,
|
||||||
clientCode,
|
clientCode,
|
||||||
licenseKey,
|
licenseKey,
|
||||||
installationCode,
|
installationCode,
|
||||||
installationName,
|
installationName,
|
||||||
dockerImage,
|
dockerImage,
|
||||||
|
dockerRegistry,
|
||||||
|
dockerUsername,
|
||||||
|
dockerPassword,
|
||||||
Path.of(outputDirectory).toAbsolutePath().normalize(),
|
Path.of(outputDirectory).toAbsolutePath().normalize(),
|
||||||
optionalFields),
|
optionalFields,
|
||||||
|
new RuntimeConfiguration(
|
||||||
|
containerCloudServiceUrl,
|
||||||
|
databaseUrl,
|
||||||
|
databaseUsername,
|
||||||
|
databasePassword,
|
||||||
|
redisHost,
|
||||||
|
redisPort,
|
||||||
|
redisPassword,
|
||||||
|
Boolean.parseBoolean(redisSsl),
|
||||||
|
"PT10S",
|
||||||
|
"PT30S")),
|
||||||
(percentage, message) ->
|
(percentage, message) ->
|
||||||
System.out.println("[" + percentage + "%] " + message));
|
System.out.println("[" + percentage + "%] " + message));
|
||||||
System.out.println("Installation registered: " + result.installationId());
|
System.out.println("Installation registered: " + result.installationId());
|
||||||
|
System.out.println("Service status: " + result.serviceStatus());
|
||||||
|
System.out.println("Service URL: " + result.serviceUrl());
|
||||||
System.out.println("Deployment files created at: " + result.outputDirectory());
|
System.out.println("Deployment files created at: " + result.outputDirectory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ public record ProductProfile(
|
|||||||
String configurationFileName,
|
String configurationFileName,
|
||||||
String installationConfigEnvironmentVariable,
|
String installationConfigEnvironmentVariable,
|
||||||
String containerConfigurationPath,
|
String containerConfigurationPath,
|
||||||
|
String portMapping,
|
||||||
|
String servicePath,
|
||||||
String tokenPath,
|
String tokenPath,
|
||||||
Path assertionEncryptionPublicKey,
|
Path assertionEncryptionPublicKey,
|
||||||
|
Path loginEncryptionPublicKey,
|
||||||
|
String loginKeyId,
|
||||||
List<String> optionalInstallationFields) {}
|
List<String> optionalInstallationFields) {}
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ public class ProductProfileRegistry {
|
|||||||
throw new InstallerConfigurationException(
|
throw new InstallerConfigurationException(
|
||||||
"Assertion encryption public key was not found: " + publicKey);
|
"Assertion encryption public key was not found: " + publicKey);
|
||||||
}
|
}
|
||||||
|
Path loginPublicKey = resolvePath(
|
||||||
|
settings.configurationDirectory(),
|
||||||
|
ini.optional(
|
||||||
|
section,
|
||||||
|
"login_encryption_public_key",
|
||||||
|
ini.required(section, "assertion_encryption_public_key")));
|
||||||
|
if (!Files.isRegularFile(loginPublicKey)) {
|
||||||
|
throw new InstallerConfigurationException(
|
||||||
|
"Login encryption public key was not found: " + loginPublicKey);
|
||||||
|
}
|
||||||
return new ProductProfile(
|
return new ProductProfile(
|
||||||
settings.product(),
|
settings.product(),
|
||||||
ini.required(section, "display_name"),
|
ini.required(section, "display_name"),
|
||||||
@@ -42,8 +52,12 @@ public class ProductProfileRegistry {
|
|||||||
"installation_config_environment_variable"),
|
"installation_config_environment_variable"),
|
||||||
absoluteContainerPath(
|
absoluteContainerPath(
|
||||||
ini.required(section, "container_configuration_path")),
|
ini.required(section, "container_configuration_path")),
|
||||||
|
portMapping(ini.required(section, "port_mapping")),
|
||||||
|
absoluteApiPath(ini.optional(section, "service_path", "/")),
|
||||||
absoluteApiPath(ini.required(section, "token_path")),
|
absoluteApiPath(ini.required(section, "token_path")),
|
||||||
publicKey,
|
publicKey,
|
||||||
|
loginPublicKey,
|
||||||
|
ini.optional(section, "login_key_id", "cygnus-login-2026-01"),
|
||||||
commaSeparated(ini.optional(section, "optional_fields", "")));
|
commaSeparated(ini.optional(section, "optional_fields", "")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +91,14 @@ public class ProductProfileRegistry {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String portMapping(String value) {
|
||||||
|
if (!value.matches("[0-9]{1,5}:[0-9]{1,5}")) {
|
||||||
|
throw new InstallerConfigurationException(
|
||||||
|
"port_mapping must use hostPort:containerPort");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> commaSeparated(String value) {
|
private List<String> commaSeparated(String value) {
|
||||||
if (value.isBlank()) {
|
if (value.isBlank()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import javax.swing.BorderFactory;
|
|||||||
import javax.swing.Box;
|
import javax.swing.Box;
|
||||||
import javax.swing.BoxLayout;
|
import javax.swing.BoxLayout;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JCheckBox;
|
||||||
import javax.swing.JFileChooser;
|
import javax.swing.JFileChooser;
|
||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
@@ -42,6 +43,7 @@ public class SwingInstallationWizard {
|
|||||||
|
|
||||||
private static final String WELCOME = "welcome";
|
private static final String WELCOME = "welcome";
|
||||||
private static final String DETAILS = "details";
|
private static final String DETAILS = "details";
|
||||||
|
private static final String RUNTIME = "runtime";
|
||||||
private static final String INSTALLING = "installing";
|
private static final String INSTALLING = "installing";
|
||||||
private static final Color ACCENT = new Color(35, 97, 146);
|
private static final Color ACCENT = new Color(35, 97, 146);
|
||||||
private static final Color BACKGROUND = new Color(236, 243, 247);
|
private static final Color BACKGROUND = new Color(236, 243, 247);
|
||||||
@@ -55,16 +57,32 @@ public class SwingInstallationWizard {
|
|||||||
private JFrame frame;
|
private JFrame frame;
|
||||||
private CardLayout cards;
|
private CardLayout cards;
|
||||||
private JPanel cardPanel;
|
private JPanel cardPanel;
|
||||||
|
private JTextField installerCloudServiceUrl;
|
||||||
private JTextField clientCode;
|
private JTextField clientCode;
|
||||||
private JPasswordField licenseKey;
|
private JPasswordField licenseKey;
|
||||||
private JTextField installationCode;
|
private JTextField installationCode;
|
||||||
private JTextField installationName;
|
private JTextField installationName;
|
||||||
private JTextField dockerImage;
|
private JTextField dockerImage;
|
||||||
|
private JTextField dockerRegistry;
|
||||||
|
private JTextField dockerUsername;
|
||||||
|
private JPasswordField dockerPassword;
|
||||||
private JTextField outputDirectory;
|
private JTextField outputDirectory;
|
||||||
|
private JTextField containerCloudServiceUrl;
|
||||||
|
private JTextField databaseUrl;
|
||||||
|
private JTextField databaseUsername;
|
||||||
|
private JPasswordField databasePassword;
|
||||||
|
private JTextField redisHost;
|
||||||
|
private JTextField redisPort;
|
||||||
|
private JPasswordField redisPassword;
|
||||||
|
private JCheckBox redisSsl;
|
||||||
|
private JTextField cloudRequestTimeout;
|
||||||
|
private JTextField tokenRefreshSkew;
|
||||||
private final Map<String, JTextField> optionalFields = new LinkedHashMap<>();
|
private final Map<String, JTextField> optionalFields = new LinkedHashMap<>();
|
||||||
private JProgressBar progressBar;
|
private JProgressBar progressBar;
|
||||||
|
private JLabel progressPercentage;
|
||||||
private JLabel progressMessage;
|
private JLabel progressMessage;
|
||||||
private JTextArea resultText;
|
private JTextArea resultText;
|
||||||
|
private JButton backToConfigurationButton;
|
||||||
private JButton closeButton;
|
private JButton closeButton;
|
||||||
|
|
||||||
public SwingInstallationWizard(
|
public SwingInstallationWizard(
|
||||||
@@ -122,6 +140,7 @@ public class SwingInstallationWizard {
|
|||||||
cardPanel.setBackground(BACKGROUND);
|
cardPanel.setBackground(BACKGROUND);
|
||||||
cardPanel.add(welcomePanel(), WELCOME);
|
cardPanel.add(welcomePanel(), WELCOME);
|
||||||
cardPanel.add(detailsPanel(), DETAILS);
|
cardPanel.add(detailsPanel(), DETAILS);
|
||||||
|
cardPanel.add(runtimePanel(), RUNTIME);
|
||||||
cardPanel.add(installingPanel(), INSTALLING);
|
cardPanel.add(installingPanel(), INSTALLING);
|
||||||
root.add(cardPanel, BorderLayout.CENTER);
|
root.add(cardPanel, BorderLayout.CENTER);
|
||||||
return root;
|
return root;
|
||||||
@@ -156,7 +175,11 @@ public class SwingInstallationWizard {
|
|||||||
+ "and creates the protected on-premises deployment package."));
|
+ "and creates the protected on-premises deployment package."));
|
||||||
copy.add(Box.createVerticalStrut(22));
|
copy.add(Box.createVerticalStrut(22));
|
||||||
copy.add(summaryRow("Product", profile.displayName()));
|
copy.add(summaryRow("Product", profile.displayName()));
|
||||||
copy.add(summaryRow("Cloud service", settings.cloudServiceUrl().toString()));
|
installerCloudServiceUrl =
|
||||||
|
new JTextField(settings.cloudServiceUrl().toString());
|
||||||
|
installerCloudServiceUrl.setEditable(true);
|
||||||
|
installerCloudServiceUrl.setEnabled(true);
|
||||||
|
copy.add(editableSummaryRow("Cloud service", installerCloudServiceUrl));
|
||||||
copy.add(summaryRow("Environment", settings.environment()));
|
copy.add(summaryRow("Environment", settings.environment()));
|
||||||
copy.add(summaryRow("Configuration", settings.configurationDirectory().toString()));
|
copy.add(summaryRow("Configuration", settings.configurationDirectory().toString()));
|
||||||
panel.add(copy, BorderLayout.CENTER);
|
panel.add(copy, BorderLayout.CENTER);
|
||||||
@@ -186,7 +209,14 @@ public class SwingInstallationWizard {
|
|||||||
licenseKey = new JPasswordField();
|
licenseKey = new JPasswordField();
|
||||||
installationCode = new JTextField();
|
installationCode = new JTextField();
|
||||||
installationName = new JTextField();
|
installationName = new JTextField();
|
||||||
|
containerCloudServiceUrl =
|
||||||
|
new JTextField(settings.containerCloudServiceUrl().toString());
|
||||||
|
containerCloudServiceUrl.setEditable(true);
|
||||||
|
containerCloudServiceUrl.setEnabled(true);
|
||||||
dockerImage = new JTextField();
|
dockerImage = new JTextField();
|
||||||
|
dockerRegistry = new JTextField();
|
||||||
|
dockerUsername = new JTextField();
|
||||||
|
dockerPassword = new JPasswordField();
|
||||||
outputDirectory = new JTextField(
|
outputDirectory = new JTextField(
|
||||||
settings.defaultOutputDirectory().toAbsolutePath().normalize().toString());
|
settings.defaultOutputDirectory().toAbsolutePath().normalize().toString());
|
||||||
int row = 0;
|
int row = 0;
|
||||||
@@ -194,12 +224,21 @@ public class SwingInstallationWizard {
|
|||||||
row = addField(form, row, "License key", licenseKey, null);
|
row = addField(form, row, "License key", licenseKey, null);
|
||||||
row = addField(form, row, "Installation code", installationCode, null);
|
row = addField(form, row, "Installation code", installationCode, null);
|
||||||
row = addField(form, row, "Installation name", installationName, null);
|
row = addField(form, row, "Installation name", installationName, null);
|
||||||
|
row = addField(
|
||||||
|
form,
|
||||||
|
row,
|
||||||
|
"Cloud service URL for container",
|
||||||
|
containerCloudServiceUrl,
|
||||||
|
null);
|
||||||
row = addField(
|
row = addField(
|
||||||
form,
|
form,
|
||||||
row,
|
row,
|
||||||
profile.displayName() + " Docker image",
|
profile.displayName() + " Docker image",
|
||||||
dockerImage,
|
dockerImage,
|
||||||
null);
|
null);
|
||||||
|
row = addField(form, row, "Docker registry", dockerRegistry, null);
|
||||||
|
row = addField(form, row, "Docker username", dockerUsername, null);
|
||||||
|
row = addField(form, row, "Docker password", dockerPassword, null);
|
||||||
for (String field : profile.optionalInstallationFields()) {
|
for (String field : profile.optionalInstallationFields()) {
|
||||||
JTextField input = new JTextField();
|
JTextField input = new JTextField();
|
||||||
optionalFields.put(field, input);
|
optionalFields.put(field, input);
|
||||||
@@ -216,6 +255,56 @@ public class SwingInstallationWizard {
|
|||||||
|
|
||||||
JButton back = secondaryButton("Back");
|
JButton back = secondaryButton("Back");
|
||||||
back.addActionListener(event -> cards.show(cardPanel, WELCOME));
|
back.addActionListener(event -> cards.show(cardPanel, WELCOME));
|
||||||
|
JButton next = primaryButton("Next");
|
||||||
|
next.addActionListener(event -> cards.show(cardPanel, RUNTIME));
|
||||||
|
panel.add(actions(back, next), BorderLayout.SOUTH);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel runtimePanel() {
|
||||||
|
JPanel panel = contentPanel();
|
||||||
|
panel.setLayout(new BorderLayout(0, 12));
|
||||||
|
|
||||||
|
JPanel top = new JPanel(new BorderLayout());
|
||||||
|
top.setOpaque(false);
|
||||||
|
top.add(heading("Runtime configuration"), BorderLayout.NORTH);
|
||||||
|
top.add(text("Configure the database and Redis services used by the "
|
||||||
|
+ profile.displayName() + " on-premises container."), BorderLayout.SOUTH);
|
||||||
|
panel.add(top, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
JPanel form = new JPanel(new GridBagLayout());
|
||||||
|
form.setBackground(PANEL);
|
||||||
|
form.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createLineBorder(new Color(205, 217, 225)),
|
||||||
|
BorderFactory.createEmptyBorder(14, 16, 14, 16)));
|
||||||
|
|
||||||
|
databaseUrl = new JTextField("jdbc:postgresql://localhost:5432/matrix");
|
||||||
|
databaseUsername = new JTextField("postgres");
|
||||||
|
databasePassword = new JPasswordField();
|
||||||
|
redisHost = new JTextField("localhost");
|
||||||
|
redisPort = new JTextField("7901");
|
||||||
|
redisPassword = new JPasswordField();
|
||||||
|
cloudRequestTimeout = new JTextField("PT10S");
|
||||||
|
tokenRefreshSkew = new JTextField("PT30S");
|
||||||
|
int row = 0;
|
||||||
|
row = addField(form, row, "Database URL", databaseUrl, null);
|
||||||
|
row = addField(form, row, "Database username", databaseUsername, null);
|
||||||
|
row = addField(form, row, "Database password", databasePassword, null);
|
||||||
|
row = addField(form, row, "Redis host", redisHost, null);
|
||||||
|
row = addField(form, row, "Redis port", redisPort, null);
|
||||||
|
row = addField(form, row, "Redis password", redisPassword, null);
|
||||||
|
row = addField(form, row, "Cloud request timeout", cloudRequestTimeout, null);
|
||||||
|
row = addField(form, row, "Token refresh skew", tokenRefreshSkew, null);
|
||||||
|
|
||||||
|
redisSsl = new JCheckBox("Use TLS/SSL for Redis");
|
||||||
|
redisSsl.setOpaque(false);
|
||||||
|
GridBagConstraints sslConstraints = constraints(1, row);
|
||||||
|
sslConstraints.anchor = GridBagConstraints.LINE_START;
|
||||||
|
form.add(redisSsl, sslConstraints);
|
||||||
|
panel.add(form, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JButton back = secondaryButton("Back");
|
||||||
|
back.addActionListener(event -> cards.show(cardPanel, DETAILS));
|
||||||
JButton install = primaryButton("Install");
|
JButton install = primaryButton("Install");
|
||||||
install.addActionListener(event -> startInstallation());
|
install.addActionListener(event -> startInstallation());
|
||||||
panel.add(actions(back, install), BorderLayout.SOUTH);
|
panel.add(actions(back, install), BorderLayout.SOUTH);
|
||||||
@@ -228,35 +317,89 @@ public class SwingInstallationWizard {
|
|||||||
JPanel center = new JPanel();
|
JPanel center = new JPanel();
|
||||||
center.setOpaque(false);
|
center.setOpaque(false);
|
||||||
center.setLayout(new BoxLayout(center, BoxLayout.Y_AXIS));
|
center.setLayout(new BoxLayout(center, BoxLayout.Y_AXIS));
|
||||||
center.add(heading("Installing " + profile.displayName()));
|
|
||||||
center.add(Box.createVerticalStrut(20));
|
JPanel progressSection = new JPanel();
|
||||||
|
progressSection.setOpaque(false);
|
||||||
|
progressSection.setLayout(new BoxLayout(progressSection, BoxLayout.Y_AXIS));
|
||||||
|
progressSection.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
progressSection.setMaximumSize(new Dimension(Integer.MAX_VALUE, 110));
|
||||||
|
|
||||||
|
JLabel installingTitle = heading("Installing " + profile.displayName());
|
||||||
|
installingTitle.setHorizontalAlignment(SwingConstants.CENTER);
|
||||||
|
installingTitle.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
installingTitle.setMaximumSize(new Dimension(Integer.MAX_VALUE, 24));
|
||||||
|
progressSection.add(installingTitle);
|
||||||
|
progressSection.add(Box.createVerticalStrut(20));
|
||||||
|
|
||||||
progressBar = new JProgressBar(0, 100);
|
progressBar = new JProgressBar(0, 100);
|
||||||
progressBar.setStringPainted(true);
|
progressBar.setStringPainted(false);
|
||||||
progressBar.setAlignmentX(Component.LEFT_ALIGNMENT);
|
progressBar.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
progressBar.setMaximumSize(new Dimension(Integer.MAX_VALUE, 28));
|
progressBar.setMaximumSize(new Dimension(Integer.MAX_VALUE, 12));
|
||||||
center.add(progressBar);
|
progressBar.setPreferredSize(new Dimension(600, 12));
|
||||||
center.add(Box.createVerticalStrut(10));
|
progressSection.add(progressBar);
|
||||||
|
progressSection.add(Box.createVerticalStrut(4));
|
||||||
|
|
||||||
|
progressPercentage = new JLabel("0%");
|
||||||
|
progressPercentage.setHorizontalAlignment(SwingConstants.RIGHT);
|
||||||
|
progressPercentage.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
progressPercentage.setMaximumSize(new Dimension(Integer.MAX_VALUE, 18));
|
||||||
|
progressSection.add(progressPercentage);
|
||||||
|
progressSection.add(Box.createVerticalStrut(6));
|
||||||
|
|
||||||
progressMessage = text("Preparing installation");
|
progressMessage = text("Preparing installation");
|
||||||
center.add(progressMessage);
|
progressMessage.setHorizontalAlignment(SwingConstants.CENTER);
|
||||||
|
progressMessage.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20));
|
||||||
|
progressSection.add(progressMessage);
|
||||||
|
center.add(progressSection);
|
||||||
center.add(Box.createVerticalStrut(20));
|
center.add(Box.createVerticalStrut(20));
|
||||||
|
|
||||||
resultText = new JTextArea(7, 40);
|
resultText = new JTextArea(7, 40);
|
||||||
resultText.setEditable(false);
|
resultText.setEditable(false);
|
||||||
resultText.setLineWrap(true);
|
resultText.setLineWrap(true);
|
||||||
resultText.setWrapStyleWord(true);
|
resultText.setWrapStyleWord(true);
|
||||||
resultText.setBackground(PANEL);
|
resultText.setBackground(PANEL);
|
||||||
resultText.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
resultText.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||||
|
resultText.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
resultText.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
|
||||||
resultText.setVisible(false);
|
resultText.setVisible(false);
|
||||||
center.add(resultText);
|
center.add(resultText);
|
||||||
panel.add(center, BorderLayout.CENTER);
|
panel.add(center, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
backToConfigurationButton = secondaryButton("Back to configuration");
|
||||||
|
backToConfigurationButton.setVisible(false);
|
||||||
|
backToConfigurationButton.addActionListener(event -> {
|
||||||
|
resultText.setVisible(false);
|
||||||
|
resultText.setForeground(UIManager.getColor("TextArea.foreground"));
|
||||||
|
progressBar.setValue(0);
|
||||||
|
progressPercentage.setText("0%");
|
||||||
|
progressMessage.setText("Preparing installation");
|
||||||
|
cards.show(cardPanel, RUNTIME);
|
||||||
|
containerCloudServiceUrl.requestFocusInWindow();
|
||||||
|
});
|
||||||
|
|
||||||
closeButton = primaryButton("Close");
|
closeButton = primaryButton("Close");
|
||||||
closeButton.setEnabled(false);
|
closeButton.setEnabled(false);
|
||||||
closeButton.addActionListener(event -> close());
|
closeButton.addActionListener(event -> close());
|
||||||
panel.add(actions(null, closeButton), BorderLayout.SOUTH);
|
panel.add(actions(backToConfigurationButton, closeButton), BorderLayout.SOUTH);
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPrerequisites(JButton button) {
|
private void checkPrerequisites(JButton button) {
|
||||||
|
try {
|
||||||
|
validateAbsoluteHttpUrl(
|
||||||
|
installerCloudServiceUrl.getText(),
|
||||||
|
"Cloud service URL");
|
||||||
|
if (containerCloudServiceUrl.getText().trim().equals(
|
||||||
|
settings.containerCloudServiceUrl().toString())
|
||||||
|
&& !installerCloudServiceUrl.getText().trim().equals(
|
||||||
|
settings.cloudServiceUrl().toString())) {
|
||||||
|
containerCloudServiceUrl.setText(
|
||||||
|
installerCloudServiceUrl.getText().trim());
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
showError(exception.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
button.setEnabled(false);
|
button.setEnabled(false);
|
||||||
button.setText("Checking Docker…");
|
button.setText("Checking Docker…");
|
||||||
new SwingWorker<Void, Void>() {
|
new SwingWorker<Void, Void>() {
|
||||||
@@ -283,25 +426,51 @@ public class SwingInstallationWizard {
|
|||||||
|
|
||||||
private void startInstallation() {
|
private void startInstallation() {
|
||||||
char[] secret = licenseKey.getPassword();
|
char[] secret = licenseKey.getPassword();
|
||||||
|
char[] registrySecret = dockerPassword.getPassword();
|
||||||
|
char[] databaseSecret = databasePassword.getPassword();
|
||||||
|
char[] redisSecret = redisPassword.getPassword();
|
||||||
InstallationRequest request;
|
InstallationRequest request;
|
||||||
try {
|
try {
|
||||||
request = new InstallationRequest(
|
request = new InstallationRequest(
|
||||||
|
installerCloudServiceUrl.getText(),
|
||||||
clientCode.getText(),
|
clientCode.getText(),
|
||||||
new String(secret),
|
new String(secret),
|
||||||
installationCode.getText(),
|
installationCode.getText(),
|
||||||
installationName.getText(),
|
installationName.getText(),
|
||||||
dockerImage.getText(),
|
dockerImage.getText(),
|
||||||
|
dockerRegistry.getText(),
|
||||||
|
dockerUsername.getText(),
|
||||||
|
new String(registrySecret),
|
||||||
Path.of(outputDirectory.getText().trim()),
|
Path.of(outputDirectory.getText().trim()),
|
||||||
optionalValues());
|
optionalValues(),
|
||||||
|
new RuntimeConfiguration(
|
||||||
|
containerCloudServiceUrl.getText(),
|
||||||
|
databaseUrl.getText(),
|
||||||
|
databaseUsername.getText(),
|
||||||
|
new String(databaseSecret),
|
||||||
|
redisHost.getText(),
|
||||||
|
redisPort.getText(),
|
||||||
|
new String(redisSecret),
|
||||||
|
redisSsl.isSelected(),
|
||||||
|
cloudRequestTimeout.getText(),
|
||||||
|
tokenRefreshSkew.getText()));
|
||||||
validateForm(request);
|
validateForm(request);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
Arrays.fill(secret, '\0');
|
Arrays.fill(secret, '\0');
|
||||||
|
Arrays.fill(registrySecret, '\0');
|
||||||
|
Arrays.fill(databaseSecret, '\0');
|
||||||
|
Arrays.fill(redisSecret, '\0');
|
||||||
showError(exception.getMessage());
|
showError(exception.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Arrays.fill(secret, '\0');
|
Arrays.fill(secret, '\0');
|
||||||
|
Arrays.fill(registrySecret, '\0');
|
||||||
|
Arrays.fill(databaseSecret, '\0');
|
||||||
|
Arrays.fill(redisSecret, '\0');
|
||||||
cards.show(cardPanel, INSTALLING);
|
cards.show(cardPanel, INSTALLING);
|
||||||
|
backToConfigurationButton.setVisible(false);
|
||||||
closeButton.setEnabled(false);
|
closeButton.setEnabled(false);
|
||||||
|
resultText.setForeground(UIManager.getColor("TextArea.foreground"));
|
||||||
resultText.setVisible(false);
|
resultText.setVisible(false);
|
||||||
|
|
||||||
new SwingWorker<InstallationResult, ProgressUpdate>() {
|
new SwingWorker<InstallationResult, ProgressUpdate>() {
|
||||||
@@ -317,6 +486,7 @@ public class SwingInstallationWizard {
|
|||||||
protected void process(java.util.List<ProgressUpdate> updates) {
|
protected void process(java.util.List<ProgressUpdate> updates) {
|
||||||
ProgressUpdate update = updates.getLast();
|
ProgressUpdate update = updates.getLast();
|
||||||
progressBar.setValue(update.percentage());
|
progressBar.setValue(update.percentage());
|
||||||
|
progressPercentage.setText(update.percentage() + "%");
|
||||||
progressMessage.setText(update.message());
|
progressMessage.setText(update.message());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,27 +496,38 @@ public class SwingInstallationWizard {
|
|||||||
try {
|
try {
|
||||||
InstallationResult result = get();
|
InstallationResult result = get();
|
||||||
progressBar.setValue(100);
|
progressBar.setValue(100);
|
||||||
|
progressPercentage.setText("100%");
|
||||||
progressMessage.setText("Installation completed");
|
progressMessage.setText("Installation completed");
|
||||||
resultText.setText(
|
resultText.setText(
|
||||||
"Installation ID: " + result.installationId()
|
"Installation ID: " + result.installationId()
|
||||||
+ "\nClient ID: " + result.clientId()
|
+ "\nClient ID: " + result.clientId()
|
||||||
|
+ "\nService: " + result.serviceName()
|
||||||
|
+ "\nStatus: " + result.serviceStatus()
|
||||||
|
+ "\nService URL: " + result.serviceUrl()
|
||||||
+ "\nOutput directory: " + result.outputDirectory()
|
+ "\nOutput directory: " + result.outputDirectory()
|
||||||
+ "\n\nKeep the private key and machine assertion secure.");
|
+ "\n\nThe deployment directory is required for "
|
||||||
|
+ "restarts and upgrades. Keep it secure.");
|
||||||
resultText.setVisible(true);
|
resultText.setVisible(true);
|
||||||
resultText.revalidate();
|
resultText.revalidate();
|
||||||
|
backToConfigurationButton.setVisible(false);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
progressMessage.setText("Installation failed");
|
progressMessage.setText("Installation failed");
|
||||||
progressBar.setValue(0);
|
progressBar.setValue(0);
|
||||||
|
progressPercentage.setText("0%");
|
||||||
resultText.setText(message(exception));
|
resultText.setText(message(exception));
|
||||||
resultText.setForeground(new Color(160, 35, 35));
|
resultText.setForeground(new Color(160, 35, 35));
|
||||||
resultText.setVisible(true);
|
resultText.setVisible(true);
|
||||||
resultText.revalidate();
|
resultText.revalidate();
|
||||||
|
backToConfigurationButton.setVisible(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.execute();
|
}.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateForm(InstallationRequest request) {
|
private void validateForm(InstallationRequest request) {
|
||||||
|
validateAbsoluteHttpUrl(
|
||||||
|
request.installerCloudServiceUrl(),
|
||||||
|
"Cloud service URL");
|
||||||
if (request.clientCode().isBlank()) {
|
if (request.clientCode().isBlank()) {
|
||||||
throw new IllegalArgumentException("Client code is required.");
|
throw new IllegalArgumentException("Client code is required.");
|
||||||
}
|
}
|
||||||
@@ -362,9 +543,49 @@ public class SwingInstallationWizard {
|
|||||||
if (request.dockerImage().isBlank()) {
|
if (request.dockerImage().isBlank()) {
|
||||||
throw new IllegalArgumentException("Docker image is required.");
|
throw new IllegalArgumentException("Docker image is required.");
|
||||||
}
|
}
|
||||||
|
if (request.dockerRegistry().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Docker registry is required.");
|
||||||
|
}
|
||||||
|
if (request.dockerUsername().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Docker username is required.");
|
||||||
|
}
|
||||||
|
if (request.dockerPassword().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Docker password is required.");
|
||||||
|
}
|
||||||
if (outputDirectory.getText().isBlank()) {
|
if (outputDirectory.getText().isBlank()) {
|
||||||
throw new IllegalArgumentException("Output directory is required.");
|
throw new IllegalArgumentException("Output directory is required.");
|
||||||
}
|
}
|
||||||
|
RuntimeConfiguration runtime = request.runtimeConfiguration();
|
||||||
|
validateAbsoluteHttpUrl(
|
||||||
|
runtime.containerCloudServiceUrl(),
|
||||||
|
"Container cloud service URL");
|
||||||
|
if (runtime.databaseUrl().isBlank()
|
||||||
|
|| runtime.databaseUsername().isBlank()
|
||||||
|
|| runtime.databasePassword().isBlank()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Complete all database connection fields.");
|
||||||
|
}
|
||||||
|
if (runtime.redisHost().isBlank()
|
||||||
|
|| runtime.redisPort().isBlank()
|
||||||
|
|| runtime.redisPassword().isBlank()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Complete all Redis connection fields.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateAbsoluteHttpUrl(String value, String label) {
|
||||||
|
try {
|
||||||
|
var uri = java.net.URI.create(value.trim());
|
||||||
|
if (!uri.isAbsolute()
|
||||||
|
|| (!"http".equalsIgnoreCase(uri.getScheme())
|
||||||
|
&& !"https".equalsIgnoreCase(uri.getScheme()))
|
||||||
|
|| uri.getHost() == null) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
label + " must be an absolute HTTP or HTTPS URL.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, String> optionalValues() {
|
private Map<String, String> optionalValues() {
|
||||||
@@ -466,6 +687,20 @@ public class SwingInstallationWizard {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private JPanel editableSummaryRow(String label, JTextField input) {
|
||||||
|
JPanel row = new JPanel(new BorderLayout(18, 0));
|
||||||
|
row.setOpaque(false);
|
||||||
|
row.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
|
||||||
|
JLabel name = new JLabel(label);
|
||||||
|
name.setFont(name.getFont().deriveFont(Font.BOLD));
|
||||||
|
name.setPreferredSize(new Dimension(130, 28));
|
||||||
|
input.setPreferredSize(new Dimension(360, 28));
|
||||||
|
row.add(name, BorderLayout.WEST);
|
||||||
|
row.add(input, BorderLayout.CENTER);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
private JButton primaryButton(String text) {
|
private JButton primaryButton(String text) {
|
||||||
JButton button = new JButton(text);
|
JButton button = new JButton(text);
|
||||||
button.setBackground(ACCENT);
|
button.setBackground(ACCENT);
|
||||||
|
|||||||
@@ -46,12 +46,18 @@ class DeploymentWriterTest {
|
|||||||
"installation.yml",
|
"installation.yml",
|
||||||
"MATRIX_INSTALLATION_CONFIG",
|
"MATRIX_INSTALLATION_CONFIG",
|
||||||
"/srv/matrix/config/installation.yml",
|
"/srv/matrix/config/installation.yml",
|
||||||
|
"8080:8080",
|
||||||
|
"/matrix/",
|
||||||
"/oauth2/token",
|
"/oauth2/token",
|
||||||
temporaryDirectory.resolve("unused.pem"),
|
temporaryDirectory.resolve("unused.pem"),
|
||||||
|
temporaryDirectory.resolve("login-public.pem"),
|
||||||
|
"cygnus-login-2026-01",
|
||||||
List.of("region"));
|
List.of("region"));
|
||||||
|
Files.writeString(profile.loginEncryptionPublicKey(), "login-public-key");
|
||||||
var settings = new InstallerSettings(
|
var settings = new InstallerSettings(
|
||||||
"matrix",
|
"matrix",
|
||||||
URI.create("https://cloud.example.com"),
|
URI.create("https://cloud.example.com"),
|
||||||
|
URI.create("http://host.docker.internal:8090"),
|
||||||
"/api/v1/installations",
|
"/api/v1/installations",
|
||||||
"production",
|
"production",
|
||||||
temporaryDirectory,
|
temporaryDirectory,
|
||||||
@@ -71,22 +77,45 @@ class DeploymentWriterTest {
|
|||||||
settings,
|
settings,
|
||||||
profile,
|
profile,
|
||||||
"registry.example.com/matrix:1.0.0",
|
"registry.example.com/matrix:1.0.0",
|
||||||
Map.of("region", "north"));
|
Map.of("region", "north"),
|
||||||
|
new RuntimeConfiguration(
|
||||||
|
"http://host.docker.internal:8090",
|
||||||
|
"jdbc:postgresql://db:5432/matrix",
|
||||||
|
"postgres",
|
||||||
|
"db-secret",
|
||||||
|
"redis",
|
||||||
|
"7901",
|
||||||
|
"redis-secret",
|
||||||
|
false,
|
||||||
|
"PT10S",
|
||||||
|
"PT30S"));
|
||||||
|
|
||||||
String config = Files.readString(output.resolve("config/installation.yml"));
|
String config = Files.readString(output.resolve("config/installation.yml"));
|
||||||
String compose = Files.readString(output.resolve("compose.yml"));
|
String compose = Files.readString(output.resolve("compose.yml"));
|
||||||
assertThat(config)
|
assertThat(config)
|
||||||
.contains("client-id: \"matrix-client\"")
|
.contains("client-id: \"matrix-client\"")
|
||||||
.contains("installation-id: \"" + installationId + "\"")
|
.contains("installation-id: \"" + installationId + "\"")
|
||||||
.contains("cloud-url: \"https://cloud.example.com\"")
|
.contains("cloud-url: \"http://host.docker.internal:8090\"")
|
||||||
|
.contains("token-url: \"http://host.docker.internal:8090/oauth2/token\"")
|
||||||
.contains("region: \"north\"")
|
.contains("region: \"north\"")
|
||||||
.doesNotContain("activation-token");
|
.doesNotContain("activation-token");
|
||||||
assertThat(compose)
|
assertThat(compose)
|
||||||
.contains("matrix-onprem:")
|
.contains("matrix-onprem:")
|
||||||
|
.contains("- \"8080:8080\"")
|
||||||
.contains("./config:/srv/matrix/config:ro")
|
.contains("./config:/srv/matrix/config:ro")
|
||||||
.contains("MATRIX_INSTALLATION_CONFIG");
|
.contains("MATRIX_INSTALLATION_CONFIG");
|
||||||
assertThat(output.resolve(".env"))
|
assertThat(Files.readString(output.resolve(".env")))
|
||||||
.hasContent("MATRIX_IMAGE=registry.example.com/matrix:1.0.0\n");
|
.contains("MATRIX_IMAGE=\"registry.example.com/matrix:1.0.0\"")
|
||||||
|
.contains("MATRIX_DB_URL=\"jdbc:postgresql://db:5432/matrix\"")
|
||||||
|
.contains("REDIS_HOST=\"redis\"")
|
||||||
|
.contains("CYGNUS_CLOUD_BASE_URL=\"http://host.docker.internal:8090\"")
|
||||||
|
.contains("CYGNUS_TOKEN_URL=\"http://host.docker.internal:8090/oauth2/token\"")
|
||||||
|
.contains("CYGNUS_CLIENT_ID=\"matrix-client\"")
|
||||||
|
.contains("CYGNUS_INSTALLATION_ID=\"primary\"")
|
||||||
|
.contains("CYGNUS_CLIENT_ASSERTION=\"file:/srv/matrix/config/machine-assertion.jwt\"")
|
||||||
|
.contains("CYGNUS_LOGIN_PUBLIC_KEY=\"file:/srv/matrix/config/keys/login-public.pem\"");
|
||||||
|
assertThat(output.resolve("config/keys/login-public.pem"))
|
||||||
|
.hasContent("login-public-key");
|
||||||
assertThat(output.resolve("config/machine-assertion.jwt")).hasContent(
|
assertThat(output.resolve("config/machine-assertion.jwt")).hasContent(
|
||||||
"encrypted.assertion.value");
|
"encrypted.assertion.value");
|
||||||
assertThat(output.resolve("config/keys/client-signing-private.pem"))
|
assertThat(output.resolve("config/keys/client-signing-private.pem"))
|
||||||
|
|||||||
@@ -11,17 +11,23 @@ class InstallationServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void rejectsIncompleteRequestBeforeCallingInfrastructure() {
|
void rejectsIncompleteRequestBeforeCallingInfrastructure() {
|
||||||
InstallationService service =
|
InstallationService service =
|
||||||
new InstallationService(null, null, null, null, null, null, null);
|
new InstallationService(
|
||||||
|
null, null, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.install(
|
assertThatThrownBy(() -> service.install(
|
||||||
new InstallationRequest(
|
new InstallationRequest(
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
Path.of("."),
|
Path.of("."),
|
||||||
Map.of()),
|
Map.of(),
|
||||||
|
null),
|
||||||
(percentage, message) -> {}))
|
(percentage, message) -> {}))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Client code is required");
|
.hasMessage("Client code is required");
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ class MachineAssertionServiceTest {
|
|||||||
"installation.yml",
|
"installation.yml",
|
||||||
"MATRIX_INSTALLATION_CONFIG",
|
"MATRIX_INSTALLATION_CONFIG",
|
||||||
"/opt/matrix/config/installation.yml",
|
"/opt/matrix/config/installation.yml",
|
||||||
|
"8080:8080",
|
||||||
|
"/matrix/",
|
||||||
"/oauth2/token",
|
"/oauth2/token",
|
||||||
cloudPublicKey,
|
cloudPublicKey,
|
||||||
|
cloudPublicKey,
|
||||||
|
"cygnus-login-2026-01",
|
||||||
List.of());
|
List.of());
|
||||||
|
|
||||||
String assertion = new MachineAssertionService().generate(
|
String assertion = new MachineAssertionService().generate(
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class ProductProfileRegistryTest {
|
|||||||
void appliesProductProfileOverridesWithoutInstallerBranching() throws Exception {
|
void appliesProductProfileOverridesWithoutInstallerBranching() throws Exception {
|
||||||
Path publicKey = temporaryDirectory.resolve("cloud.pem");
|
Path publicKey = temporaryDirectory.resolve("cloud.pem");
|
||||||
Files.writeString(publicKey, "test");
|
Files.writeString(publicKey, "test");
|
||||||
|
Path loginPublicKey = temporaryDirectory.resolve("login.pem");
|
||||||
|
Files.writeString(loginPublicKey, "test-login");
|
||||||
Path ini = temporaryDirectory.resolve("installer.ini");
|
Path ini = temporaryDirectory.resolve("installer.ini");
|
||||||
Files.writeString(ini, """
|
Files.writeString(ini, """
|
||||||
[installer]
|
[installer]
|
||||||
@@ -27,12 +29,16 @@ class ProductProfileRegistryTest {
|
|||||||
[profile.cygnus]
|
[profile.cygnus]
|
||||||
display_name=Cygnus
|
display_name=Cygnus
|
||||||
assertion_encryption_public_key=cloud.pem
|
assertion_encryption_public_key=cloud.pem
|
||||||
|
login_encryption_public_key=login.pem
|
||||||
|
login_key_id=cygnus-login-2026-01
|
||||||
service_name=custom-cygnus
|
service_name=custom-cygnus
|
||||||
image_environment_variable=CYGNUS_IMAGE
|
image_environment_variable=CYGNUS_IMAGE
|
||||||
configuration_root=cygnus
|
configuration_root=cygnus
|
||||||
configuration_file_name=installation.yml
|
configuration_file_name=installation.yml
|
||||||
installation_config_environment_variable=CYGNUS_INSTALLATION_CONFIG
|
installation_config_environment_variable=CYGNUS_INSTALLATION_CONFIG
|
||||||
container_configuration_path=/opt/cygnus/config/installation.yml
|
container_configuration_path=/opt/cygnus/config/installation.yml
|
||||||
|
port_mapping=8080:8080
|
||||||
|
service_path=/matrix/
|
||||||
token_path=/oauth2/token
|
token_path=/oauth2/token
|
||||||
optional_fields=region,site_code
|
optional_fields=region,site_code
|
||||||
""");
|
""");
|
||||||
@@ -45,5 +51,6 @@ class ProductProfileRegistryTest {
|
|||||||
assertThat(profile.optionalInstallationFields())
|
assertThat(profile.optionalInstallationFields())
|
||||||
.containsExactly("region", "site_code");
|
.containsExactly("region", "site_code");
|
||||||
assertThat(profile.assertionEncryptionPublicKey()).isEqualTo(publicKey);
|
assertThat(profile.assertionEncryptionPublicKey()).isEqualTo(publicKey);
|
||||||
|
assertThat(profile.loginEncryptionPublicKey()).isEqualTo(loginPublicKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,11 +11,13 @@ com/cygnus/installer/InstallationWizard.class
|
|||||||
com/cygnus/installer/TechnobeeServiceInstallerApplication.class
|
com/cygnus/installer/TechnobeeServiceInstallerApplication.class
|
||||||
com/cygnus/installer/DeploymentWriter.class
|
com/cygnus/installer/DeploymentWriter.class
|
||||||
com/cygnus/installer/InstallationKeyService.class
|
com/cygnus/installer/InstallationKeyService.class
|
||||||
|
com/cygnus/installer/DockerComposeDeploymentService.class
|
||||||
com/cygnus/installer/ActivationClient.class
|
com/cygnus/installer/ActivationClient.class
|
||||||
com/cygnus/installer/ActivationDtos$RegistrationResponse.class
|
com/cygnus/installer/ActivationDtos$RegistrationResponse.class
|
||||||
com/cygnus/installer/IniDocument.class
|
com/cygnus/installer/IniDocument.class
|
||||||
com/cygnus/installer/SwingInstallationWizard$3.class
|
com/cygnus/installer/SwingInstallationWizard$3.class
|
||||||
com/cygnus/installer/InstallerConfigurationException.class
|
com/cygnus/installer/InstallerConfigurationException.class
|
||||||
|
com/cygnus/installer/DockerRegistryLoginService.class
|
||||||
com/cygnus/installer/SwingInstallationWizard.class
|
com/cygnus/installer/SwingInstallationWizard.class
|
||||||
com/cygnus/installer/ActivationDtos.class
|
com/cygnus/installer/ActivationDtos.class
|
||||||
com/cygnus/installer/DockerPrerequisiteChecker.class
|
com/cygnus/installer/DockerPrerequisiteChecker.class
|
||||||
@@ -25,6 +27,7 @@ com/cygnus/installer/CommandExecutor.class
|
|||||||
com/cygnus/installer/IniConfigurationLoader.class
|
com/cygnus/installer/IniConfigurationLoader.class
|
||||||
com/cygnus/installer/InstallationService.class
|
com/cygnus/installer/InstallationService.class
|
||||||
com/cygnus/installer/InstallationResult.class
|
com/cygnus/installer/InstallationResult.class
|
||||||
|
com/cygnus/installer/RuntimeConfiguration.class
|
||||||
com/cygnus/installer/ActivationDtos$RegistrationRequest.class
|
com/cygnus/installer/ActivationDtos$RegistrationRequest.class
|
||||||
com/cygnus/installer/ActivationDtos$ValidationRequest.class
|
com/cygnus/installer/ActivationDtos$ValidationRequest.class
|
||||||
com/cygnus/installer/PrerequisiteException.class
|
com/cygnus/installer/PrerequisiteException.class
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CommandExecutor.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CommandExecutor.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CommandResult.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CommandResult.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DeploymentWriter.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DeploymentWriter.java
|
||||||
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DockerComposeDeploymentService.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DockerPrerequisiteChecker.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DockerPrerequisiteChecker.java
|
||||||
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DockerRegistryLoginService.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/IniConfigurationLoader.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/IniConfigurationLoader.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/IniDocument.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/IniDocument.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationKeyPair.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationKeyPair.java
|
||||||
@@ -21,5 +23,6 @@
|
|||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/PrerequisiteException.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/PrerequisiteException.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ProductProfile.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ProductProfile.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ProductProfileRegistry.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ProductProfileRegistry.java
|
||||||
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/RuntimeConfiguration.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/SwingInstallationWizard.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/SwingInstallationWizard.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/TechnobeeServiceInstallerApplication.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/TechnobeeServiceInstallerApplication.java
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
com/cygnus/installer/MachineAssertionServiceTest.class
|
com/cygnus/installer/MachineAssertionServiceTest.class
|
||||||
|
com/cygnus/installer/DockerRegistryLoginServiceTest$CapturingExecutor.class
|
||||||
|
com/cygnus/installer/DockerRegistryLoginServiceTest.class
|
||||||
|
com/cygnus/installer/DockerComposeDeploymentServiceTest$CapturingExecutor.class
|
||||||
com/cygnus/installer/InstallationServiceTest.class
|
com/cygnus/installer/InstallationServiceTest.class
|
||||||
com/cygnus/installer/DockerPrerequisiteCheckerTest$1.class
|
com/cygnus/installer/DockerPrerequisiteCheckerTest$1.class
|
||||||
com/cygnus/installer/ProductProfileRegistryTest.class
|
com/cygnus/installer/ProductProfileRegistryTest.class
|
||||||
com/cygnus/installer/IniConfigurationLoaderTest.class
|
com/cygnus/installer/IniConfigurationLoaderTest.class
|
||||||
|
com/cygnus/installer/DockerComposeDeploymentServiceTest.class
|
||||||
com/cygnus/installer/DeploymentWriterTest.class
|
com/cygnus/installer/DeploymentWriterTest.class
|
||||||
com/cygnus/installer/DockerPrerequisiteCheckerTest.class
|
com/cygnus/installer/DockerPrerequisiteCheckerTest.class
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DeploymentWriterTest.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DeploymentWriterTest.java
|
||||||
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DockerComposeDeploymentServiceTest.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DockerPrerequisiteCheckerTest.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DockerPrerequisiteCheckerTest.java
|
||||||
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DockerRegistryLoginServiceTest.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/IniConfigurationLoaderTest.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/IniConfigurationLoaderTest.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/InstallationServiceTest.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/InstallationServiceTest.java
|
||||||
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/MachineAssertionServiceTest.java
|
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/MachineAssertionServiceTest.java
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
Test set: com.cygnus.installer.DeploymentWriterTest
|
Test set: com.cygnus.installer.DeploymentWriterTest
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.206 s -- in com.cygnus.installer.DeploymentWriterTest
|
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.174 s -- in com.cygnus.installer.DeploymentWriterTest
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Test set: com.cygnus.installer.DockerRegistryLoginServiceTest
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.010 s -- in com.cygnus.installer.DockerRegistryLoginServiceTest
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
Test set: com.cygnus.installer.IniConfigurationLoaderTest
|
Test set: com.cygnus.installer.IniConfigurationLoaderTest
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.006 s -- in com.cygnus.installer.IniConfigurationLoaderTest
|
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.008 s -- in com.cygnus.installer.IniConfigurationLoaderTest
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
Test set: com.cygnus.installer.MachineAssertionServiceTest
|
Test set: com.cygnus.installer.MachineAssertionServiceTest
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.301 s -- in com.cygnus.installer.MachineAssertionServiceTest
|
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.570 s -- in com.cygnus.installer.MachineAssertionServiceTest
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
Test set: com.cygnus.installer.ProductProfileRegistryTest
|
Test set: com.cygnus.installer.ProductProfileRegistryTest
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.010 s -- in com.cygnus.installer.ProductProfileRegistryTest
|
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.005 s -- in com.cygnus.installer.ProductProfileRegistryTest
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2
cygnus-onprem-app/.dockerignore
Normal file
2
cygnus-onprem-app/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!target/matrix.war
|
||||||
21
cygnus-onprem-app/Dockerfile
Normal file
21
cygnus-onprem-app/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM tomcat:10.1-jdk21-temurin
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="Technobee Matrix On-Premises Service"
|
||||||
|
LABEL org.opencontainers.image.vendor="Technobee Solutions"
|
||||||
|
|
||||||
|
RUN rm -rf /usr/local/tomcat/webapps/*
|
||||||
|
|
||||||
|
COPY target/matrix.war /usr/local/tomcat/webapps/matrix.war
|
||||||
|
|
||||||
|
RUN mkdir -p /opt/matrix/config \
|
||||||
|
&& chown -R 1000:0 /opt/matrix /usr/local/tomcat \
|
||||||
|
&& chmod -R g=u /opt/matrix /usr/local/tomcat
|
||||||
|
|
||||||
|
ENV MATRIX_INSTALLATION_CONFIG=/opt/matrix/config/installation.yml
|
||||||
|
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -Djava.awt.headless=true"
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
CMD ["catalina.sh", "run"]
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
2. One or more tenant accounts are attached to that registration.
|
2. One or more tenant accounts are attached to that registration.
|
||||||
3. A time-bound license defines package, user, and installation limits.
|
3. A time-bound license defines package, user, and installation limits.
|
||||||
4. An activation key is generated, stored only as a BCrypt hash, and emailed.
|
4. An activation key is generated, stored only as a BCrypt hash, and emailed.
|
||||||
|
The recipient is always the primary contact email stored on the client
|
||||||
|
registration; API callers cannot redirect the key to another address.
|
||||||
5. The Technobee Service Installer blocks unless Docker, Docker Engine, and
|
5. The Technobee Service Installer blocks unless Docker, Docker Engine, and
|
||||||
Compose v2 work.
|
Compose v2 work.
|
||||||
6. The installer validates the key, generates an RSA-3072 installation key, and
|
6. The installer validates the key, generates an RSA-3072 installation key, and
|
||||||
@@ -44,6 +46,9 @@ All administration endpoints require an access token with
|
|||||||
3. `POST /api/v1/admin/tenants/{tenantId}/licenses`
|
3. `POST /api/v1/admin/tenants/{tenantId}/licenses`
|
||||||
4. `POST /api/v1/admin/tenants/{tenantId}/licenses/{licenseId}/activation-key`
|
4. `POST /api/v1/admin/tenants/{tenantId}/licenses/{licenseId}/activation-key`
|
||||||
|
|
||||||
|
The activation-key request contains only `expiresAt`. The cloud service reads
|
||||||
|
the delivery address from `client_registration_details.primary_contact_email`.
|
||||||
|
|
||||||
For replacements:
|
For replacements:
|
||||||
|
|
||||||
- `POST /api/v1/admin/tenants/{tenantId}/installations/{id}/decommission`
|
- `POST /api/v1/admin/tenants/{tenantId}/installations/{id}/decommission`
|
||||||
@@ -71,6 +76,9 @@ compiled into the installer workflow.
|
|||||||
The installer asks for the Matrix or Cygnus Docker image and writes it to
|
The installer asks for the Matrix or Cygnus Docker image and writes it to
|
||||||
`.env` using the selected profile's image variable. After a successful run,
|
`.env` using the selected profile's image variable. After a successful run,
|
||||||
review `compose.yml` and `.env`, then start with `docker compose up -d`.
|
review `compose.yml` and `.env`, then start with `docker compose up -d`.
|
||||||
|
It also asks for the registry host, username, and password and performs
|
||||||
|
`docker login --password-stdin`. The password is not stored in the generated
|
||||||
|
installation package.
|
||||||
|
|
||||||
The installer asks for the output directory. Press Enter to accept the INI
|
The installer asks for the output directory. Press Enter to accept the INI
|
||||||
`output_directory`. It creates:
|
`output_directory`. It creates:
|
||||||
|
|||||||
Reference in New Issue
Block a user