Installed GUI Integration

This commit is contained in:
2026-07-26 19:48:26 +05:30
parent 60f5450f47
commit 4afe00e1f8
323 changed files with 2219 additions and 196 deletions

View File

@@ -1,26 +1,83 @@
# Cygnus installation wizard
# Technobee Service Installer
The wizard validates Docker, activates a client license, generates a unique
3072-bit installation signing key, registers the installation, and writes the
deployment files. The license key is never persisted.
One Swing-based Java installer supports Matrix and Cygnus through an external
INI product profile. It never connects to PostgreSQL or Redis. The selected
product cloud service validates the activation key and registers the
installation.
## Build and run
```bash
java -jar target/cygnus-installer-1.0.0-SNAPSHOT.jar
mvn -pl cygnus-installer -am package
TECHNOBEE_INSTALLER_CONFIG=cygnus-installer/config/matrix-installer.ini \
java -jar cygnus-installer/target/technobee-service-installer-1.0.0-SNAPSHOT.jar
```
Set `CYGNUS_CLOUD_URL` before execution. The wizard asks where deployment
files should be written and offers `CYGNUS_INSTALL_OUTPUT` as the default.
Use `config/cygnus-installer.ini` for Cygnus. Before production use, set the
cloud URL and the cloud assertion-encryption public-key path in the selected
INI. Both profiles use these cloud endpoints by default:
The selected output directory contains:
- `POST /api/v1/installations/activation/validate`
- `POST /api/v1/installations/register`
The desktop wizard is the default. It checks Docker, collects activation and
installation details, lets the user choose an output directory, and reports
installation progress. For servers without a desktop or for automation, append
`--cli` to retain the interactive terminal workflow:
```bash
TECHNOBEE_INSTALLER_CONFIG=cygnus-installer/config/matrix-installer.ini \
java -jar cygnus-installer/target/technobee-service-installer-1.0.0-SNAPSHOT.jar --cli
```
## Configuration
```ini
[installer]
product=matrix
cloud_service_url=https://cloud.example.com
installer_api_base_path=/api/v1/installations
environment=production
output_directory=./matrix-installation
[profile.matrix]
display_name=Matrix
service_name=matrix-onprem
image_environment_variable=MATRIX_IMAGE
configuration_root=matrix
configuration_file_name=installation.yml
installation_config_environment_variable=MATRIX_INSTALLATION_CONFIG
container_configuration_path=/opt/matrix/config/installation.yml
token_path=/oauth2/token
assertion_encryption_public_key=/secure/cloud-assertion-public.pem
optional_fields=
```
The installer validates the product, URL, API path, environment, profile
values, and public-key file before asking installation questions.
## Generated output
```text
.env
compose.yml
config/
installation.yml
machine-assertion.jwt
keys/
client-signing-private.pem
client-signing-public.pem
```
The private key is created with owner-only permissions on POSIX systems. The
plaintext installation license key is never written to the output directory.
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
automatically, so no separate `export MATRIX_IMAGE` or `export CYGNUS_IMAGE`
step is required.
The RSA-3072 pair is generated locally. Only the public key is sent during
registration. The private key and encrypted machine assertion remain on
premises and receive owner-only permissions on POSIX systems. The activation
key is never written to disk.
`scripts/setup-local-communication.sh` remains a development/provisioning
utility; it is not used by this installer.

View File

@@ -0,0 +1,18 @@
[installer]
product=cygnus
cloud_service_url=http://localhost:8090
installer_api_base_path=/api/v1/installations
environment=production
output_directory=./cygnus-installation
[profile.cygnus]
display_name=Cygnus
service_name=cygnus-onprem
image_environment_variable=CYGNUS_IMAGE
configuration_root=cygnus
configuration_file_name=installation.yml
installation_config_environment_variable=CYGNUS_INSTALLATION_CONFIG
container_configuration_path=/opt/cygnus/config/installation.yml
token_path=/oauth2/token
assertion_encryption_public_key=../../config/keys/assertion-decryption-public.pem
optional_fields=

View File

@@ -0,0 +1,18 @@
[installer]
product=matrix
cloud_service_url=http://localhost:8090
installer_api_base_path=/api/v1/installations
environment=production
output_directory=./matrix-installation
[profile.matrix]
display_name=Matrix
service_name=matrix-onprem
image_environment_variable=MATRIX_IMAGE
configuration_root=matrix
configuration_file_name=installation.yml
installation_config_environment_variable=MATRIX_INSTALLATION_CONFIG
container_configuration_path=/opt/matrix/config/installation.yml
token_path=/oauth2/token
assertion_encryption_public_key=../../config/keys/assertion-decryption-public.pem
optional_fields=

View File

@@ -9,8 +9,8 @@
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>cygnus-installer</artifactId>
<name>Cygnus Installation Wizard</name>
<artifactId>technobee-service-installer</artifactId>
<name>Technobee Service Installer</name>
<dependencyManagement>
<dependencies>
<dependency>
@@ -27,6 +27,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux-test</artifactId>

View File

@@ -8,9 +8,11 @@ import org.springframework.web.reactive.function.client.WebClient;
public class ActivationClient {
private final WebClient webClient;
private final InstallerSettings settings;
public ActivationClient(WebClient webClient) {
public ActivationClient(WebClient webClient, InstallerSettings settings) {
this.webClient = webClient;
this.settings = settings;
}
public ActivationDtos.ValidationResponse validate(
@@ -19,7 +21,7 @@ public class ActivationClient {
UUID installationUuid,
String installerVersion) {
return webClient.post()
.uri("/api/v1/installations/activation/validate")
.uri(settings.validationPath())
.bodyValue(new ActivationDtos.ValidationRequest(
clientCode, licenseKey, installationUuid, installerVersion))
.retrieve()
@@ -35,7 +37,7 @@ public class ActivationClient {
String softwareVersion,
String environment) {
return webClient.post()
.uri("/api/v1/installations/register")
.uri(settings.registrationPath())
.bodyValue(new ActivationDtos.RegistrationRequest(
validation.activationToken(),
installationCode,

View File

@@ -30,8 +30,10 @@ final class ActivationDtos {
record RegistrationResponse(
UUID installationId,
UUID installationUuid,
UUID tenantId,
UUID clientId,
String clientId,
String installationCode,
int securityVersion,
String status) {}
}

View File

@@ -1,27 +0,0 @@
package com.cygnus.installer;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.WebApplicationType;
@SpringBootApplication
public class CygnusInstallerApplication implements CommandLineRunner {
private final InstallationWizard wizard;
public CygnusInstallerApplication(InstallationWizard wizard) {
this.wizard = wizard;
}
public static void main(String[] args) {
SpringApplication application = new SpringApplication(CygnusInstallerApplication.class);
application.setWebApplicationType(WebApplicationType.NONE);
System.exit(SpringApplication.exit(application.run(args)));
}
@Override
public void run(String... args) {
wizard.run();
}
}

View File

@@ -1,9 +1,12 @@
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;
@@ -14,55 +17,161 @@ public class DeploymentWriter {
Path output,
UUID installationUuid,
String installationCode,
String clientId,
ActivationDtos.ValidationResponse validation,
ActivationDtos.RegistrationResponse registration,
InstallationKeyPair keys,
String cloudUrl) {
String machineAssertion,
InstallerSettings settings,
ProductProfile profile,
String dockerImage,
Map<String, String> optionalFields) {
try {
Path config = output.resolve("config");
Path normalizedOutput = output.toAbsolutePath().normalize();
Path config = normalizedOutput.resolve("config");
Path keyDirectory = config.resolve("keys");
Files.createDirectories(keyDirectory);
writePrivate(keyDirectory.resolve("client-signing-private.pem"), keys.privateKeyPem());
Files.writeString(
keyDirectory.resolve("client-signing-public.pem"), keys.publicKeyPem());
Files.writeString(config.resolve("installation.yml"), """
cygnus:
cloud-url: "%s"
tenant-id: "%s"
client-id: "%s"
installation-id: "%s"
installation-uuid: "%s"
installation-code: "%s"
signing-private-key: "file:./config/keys/client-signing-private.pem"
""".formatted(
cloudUrl,
validation.tenantId(),
registration.clientId(),
registration.installationId(),
installationUuid,
installationCode));
Files.writeString(output.resolve("compose.yml"), """
services:
cygnus-onprem:
image: ${CYGNUS_IMAGE:?Set CYGNUS_IMAGE}
restart: unless-stopped
volumes:
- ./config:/opt/cygnus/config:ro
environment:
CYGNUS_INSTALLATION_CONFIG: /opt/cygnus/config/installation.yml
""");
return output;
} catch (IOException e) {
throw new IllegalStateException("Could not write installation files", e);
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());
writePrivate(
normalizedOutput.resolve(assertionRelative),
machineAssertion);
writePublic(
config.resolve(profile.configurationFileName()),
installationConfiguration(
installationUuid,
installationCode,
clientId,
validation,
registration,
settings,
profile,
privateRelative,
publicRelative,
assertionRelative,
optionalFields));
writePublic(
normalizedOutput.resolve("compose.yml"),
compose(profile));
writePrivate(
normalizedOutput.resolve(".env"),
profile.imageEnvironmentVariable() + "=" + dockerImage + "\n");
return normalizedOutput;
} catch (IOException exception) {
throw new IllegalStateException("Could not write installation files", exception);
}
}
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) {
StringBuilder yaml = new StringBuilder()
.append(profile.configurationRoot()).append(":\n")
.append(" product: ").append(quoted(profile.product())).append('\n')
.append(" cloud-url: ")
.append(quoted(settings.cloudServiceUrl().toString())).append('\n')
.append(" token-url: ")
.append(quoted(settings.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
volumes:
- ./config:%s:ro
environment:
%s: %s
""".formatted(
profile.serviceName(),
profile.imageEnvironmentVariable(),
profile.imageEnvironmentVariable(),
containerConfigDirectory,
profile.installationConfigEnvironmentVariable(),
profile.containerConfigurationPath());
}
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.writeString(path, content);
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 their native ACLs.
// 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);
}
}

View File

@@ -0,0 +1,119 @@
package com.cygnus.installer;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Component;
@Component
public class IniConfigurationLoader {
private static final Set<String> PRODUCTS = Set.of("matrix", "cygnus");
public InstallerSettings load(Path path, String installerVersion) {
IniDocument ini = parse(path);
String product = ini.required("installer", "product").toLowerCase(Locale.ROOT);
if (!PRODUCTS.contains(product)) {
throw new InstallerConfigurationException(
"Unsupported product '" + product + "'. Expected matrix or cygnus");
}
URI cloudUri = absoluteHttpUri(ini.required("installer", "cloud_service_url"));
String apiBasePath = normalizedApiPath(
ini.required("installer", "installer_api_base_path"));
String environment = ini.required("installer", "environment");
String output = ini.optional(
"installer", "output_directory", "./" + product + "-installation");
return new InstallerSettings(
product,
cloudUri,
apiBasePath,
environment,
Path.of(output),
path.toAbsolutePath().normalize().getParent(),
installerVersion,
ini);
}
IniDocument parse(Path path) {
if (path == null || !Files.isRegularFile(path)) {
throw new InstallerConfigurationException(
"Installer INI file was not found: " + path);
}
Map<String, Map<String, String>> sections = new LinkedHashMap<>();
String currentSection = null;
try {
int lineNumber = 0;
for (String rawLine : Files.readAllLines(path)) {
lineNumber++;
String line = rawLine.trim();
if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
currentSection = line.substring(1, line.length() - 1)
.trim()
.toLowerCase(Locale.ROOT);
if (currentSection.isBlank()) {
throw malformed(path, lineNumber);
}
sections.computeIfAbsent(currentSection, ignored -> new LinkedHashMap<>());
continue;
}
int separator = line.indexOf('=');
if (currentSection == null || separator < 1) {
throw malformed(path, lineNumber);
}
String key = line.substring(0, separator).trim().toLowerCase(Locale.ROOT);
String value = line.substring(separator + 1).trim();
if (key.isBlank()) {
throw malformed(path, lineNumber);
}
sections.get(currentSection).put(key, value);
}
return new IniDocument(sections);
} catch (IOException exception) {
throw new InstallerConfigurationException(
"Unable to read installer INI file: " + path, exception);
}
}
private URI absoluteHttpUri(String value) {
try {
URI uri = URI.create(value);
if (!uri.isAbsolute()
|| !("http".equalsIgnoreCase(uri.getScheme())
|| "https".equalsIgnoreCase(uri.getScheme()))
|| uri.getHost() == null
|| uri.getQuery() != null
|| uri.getFragment() != null) {
throw new IllegalArgumentException();
}
return URI.create(value.replaceAll("/+$", ""));
} catch (IllegalArgumentException exception) {
throw new InstallerConfigurationException(
"cloud_service_url must be an absolute http(s) URL");
}
}
private String normalizedApiPath(String value) {
String path = value.trim().replaceAll("/+$", "");
if (!path.startsWith("/")
|| path.contains("..")
|| path.contains("?")
|| path.contains("#")) {
throw new InstallerConfigurationException(
"installer_api_base_path must be an absolute safe URL path");
}
return path;
}
private InstallerConfigurationException malformed(Path path, int line) {
return new InstallerConfigurationException(
"Malformed INI configuration at " + path + ":" + line);
}
}

View File

@@ -0,0 +1,29 @@
package com.cygnus.installer;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
final class IniDocument {
private final Map<String, Map<String, String>> sections;
IniDocument(Map<String, Map<String, String>> sections) {
Map<String, Map<String, String>> copy = new LinkedHashMap<>();
sections.forEach((name, values) ->
copy.put(name, Collections.unmodifiableMap(new LinkedHashMap<>(values))));
this.sections = Collections.unmodifiableMap(copy);
}
String required(String section, String key) {
String value = optional(section, key, null);
if (value == null || value.isBlank()) {
throw new InstallerConfigurationException(
"Missing required INI setting [" + section + "] " + key);
}
return value;
}
String optional(String section, String key, String defaultValue) {
return sections.getOrDefault(section, Map.of()).getOrDefault(key, defaultValue);
}
}

View File

@@ -0,0 +1,6 @@
package com.cygnus.installer;
@FunctionalInterface
public interface InstallationProgressListener {
void update(int percentage, String message);
}

View File

@@ -0,0 +1,18 @@
package com.cygnus.installer;
import java.nio.file.Path;
import java.util.Map;
public record InstallationRequest(
String clientCode,
String licenseKey,
String installationCode,
String installationName,
String dockerImage,
Path outputDirectory,
Map<String, String> optionalFields) {
public InstallationRequest {
optionalFields = optionalFields == null ? Map.of() : Map.copyOf(optionalFields);
}
}

View File

@@ -0,0 +1,10 @@
package com.cygnus.installer;
import java.nio.file.Path;
import java.util.UUID;
public record InstallationResult(
UUID installationId,
String installationCode,
String clientId,
Path outputDirectory) {}

View File

@@ -0,0 +1,125 @@
package com.cygnus.installer;
import java.nio.file.Path;
import java.util.UUID;
import org.springframework.stereotype.Service;
@Service
public class InstallationService {
private final DockerPrerequisiteChecker prerequisites;
private final ActivationClient activationClient;
private final InstallationKeyService keyService;
private final MachineAssertionService assertionService;
private final DeploymentWriter deploymentWriter;
private final InstallerSettings settings;
private final ProductProfile profile;
public InstallationService(
DockerPrerequisiteChecker prerequisites,
ActivationClient activationClient,
InstallationKeyService keyService,
MachineAssertionService assertionService,
DeploymentWriter deploymentWriter,
InstallerSettings settings,
ProductProfile profile) {
this.prerequisites = prerequisites;
this.activationClient = activationClient;
this.keyService = keyService;
this.assertionService = assertionService;
this.deploymentWriter = deploymentWriter;
this.settings = settings;
this.profile = profile;
}
public void verifyPrerequisites() {
prerequisites.verify();
}
public InstallationResult install(
InstallationRequest request,
InstallationProgressListener progress) {
validate(request);
progress.update(5, "Checking Docker prerequisites");
prerequisites.verify();
UUID installationUuid = UUID.randomUUID();
progress.update(20, "Validating activation key");
var validation = activationClient.validate(
request.clientCode().trim(),
request.licenseKey(),
installationUuid,
settings.installerVersion());
progress.update(40, "Generating local RSA signing keys");
var keys = keyService.generate();
progress.update(58, "Registering installation with "
+ profile.displayName() + " cloud service");
var registration = activationClient.register(
validation,
request.installationCode().trim(),
request.installationName().trim(),
keys.publicKeyPem(),
settings.installerVersion(),
settings.environment());
String clientId = registration.clientId() == null
|| registration.clientId().isBlank()
? validation.tenantSlug()
: registration.clientId();
progress.update(75, "Creating encrypted machine assertion");
String assertion = assertionService.generate(
clientId,
request.installationCode().trim(),
settings.cloudServiceUrl() + profile.tokenPath(),
keys,
profile);
progress.update(88, "Writing on-premises deployment files");
Path output = deploymentWriter.write(
request.outputDirectory().toAbsolutePath().normalize(),
installationUuid,
request.installationCode().trim(),
clientId,
validation,
registration,
keys,
assertion,
settings,
profile,
request.dockerImage().trim(),
request.optionalFields());
progress.update(100, "Installation package created successfully");
return new InstallationResult(
registration.installationId(),
request.installationCode().trim(),
clientId,
output);
}
private void validate(InstallationRequest request) {
if (request == null) {
throw new IllegalArgumentException("Installation request is required");
}
required(request.clientCode(), "Client code");
required(request.licenseKey(), "License key");
required(request.installationCode(), "Installation code");
required(request.installationName(), "Installation name");
required(request.dockerImage(), "Docker image");
if (!request.dockerImage().matches(
"[A-Za-z0-9][A-Za-z0-9._:/@-]{0,254}")) {
throw new IllegalArgumentException(
"Docker image contains invalid characters");
}
if (request.outputDirectory() == null) {
throw new IllegalArgumentException("Output directory is required");
}
}
private void required(String value, String label) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(label + " is required");
}
}
}

View File

@@ -2,35 +2,31 @@ package com.cygnus.installer;
import java.io.Console;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.UUID;
import org.springframework.stereotype.Service;
@Service
public class InstallationWizard {
private final DockerPrerequisiteChecker prerequisites;
private final ActivationClient activationClient;
private final InstallationKeyService keyService;
private final DeploymentWriter deploymentWriter;
private final InstallerProperties properties;
private final InstallationService installationService;
private final InstallerSettings settings;
private final ProductProfile profile;
public InstallationWizard(
DockerPrerequisiteChecker prerequisites,
ActivationClient activationClient,
InstallationKeyService keyService,
DeploymentWriter deploymentWriter,
InstallerProperties properties) {
this.prerequisites = prerequisites;
this.activationClient = activationClient;
this.keyService = keyService;
this.deploymentWriter = deploymentWriter;
this.properties = properties;
InstallationService installationService,
InstallerSettings settings,
ProductProfile profile) {
this.installationService = installationService;
this.settings = settings;
this.profile = profile;
}
public void run() {
System.out.println("Cygnus installation wizard");
prerequisites.verify();
System.out.println("Technobee Service Installer");
System.out.println("Selected product: " + profile.displayName());
installationService.verifyPrerequisites();
System.out.println("Docker prerequisites: OK");
Console console = System.console();
@@ -39,34 +35,44 @@ public class InstallationWizard {
String licenseKey = secret(console, scanner, "License key");
String installationCode = prompt(console, scanner, "Installation code");
String installationName = prompt(console, scanner, "Installation name");
String environment = prompt(console, scanner, "Environment (prod/uat/dev)");
String dockerImage = prompt(
console,
scanner,
profile.displayName() + " Docker image");
Map<String, String> optionalFields = promptOptionalFields(console, scanner);
String outputDirectory = promptWithDefault(
console,
scanner,
"Output directory",
properties.outputDirectory());
settings.defaultOutputDirectory().toString());
UUID installationUuid = UUID.randomUUID();
var validation = activationClient.validate(
clientCode, licenseKey, installationUuid, properties.version());
var keys = keyService.generate();
var registration = activationClient.register(
validation,
InstallationResult result = installationService.install(
new InstallationRequest(
clientCode,
licenseKey,
installationCode,
installationName,
keys.publicKeyPem(),
properties.version(),
environment);
Path output = deploymentWriter.write(
dockerImage,
Path.of(outputDirectory).toAbsolutePath().normalize(),
installationUuid,
installationCode,
validation,
registration,
keys,
properties.cloudUrl());
System.out.println("Installation registered: " + registration.installationId());
System.out.println("Deployment files created at: " + output);
optionalFields),
(percentage, message) ->
System.out.println("[" + percentage + "%] " + message));
System.out.println("Installation registered: " + result.installationId());
System.out.println("Deployment files created at: " + result.outputDirectory());
}
private Map<String, String> promptOptionalFields(
Console console, Scanner scanner) {
Map<String, String> values = new LinkedHashMap<>();
for (String field : profile.optionalInstallationFields()) {
values.put(field, prompt(console, scanner, displayLabel(field)));
}
return values;
}
private String displayLabel(String field) {
String words = field.replace('_', ' ').replace('-', ' ');
return Character.toUpperCase(words.charAt(0)) + words.substring(1);
}
private String prompt(Console console, Scanner scanner, String label) {

View File

@@ -0,0 +1,6 @@
package com.cygnus.installer;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "technobee.installer")
public record InstallerBootstrapProperties(String config, String version) {}

View File

@@ -1,16 +1,35 @@
package com.cygnus.installer;
import java.nio.file.Path;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
@EnableConfigurationProperties(InstallerProperties.class)
@EnableConfigurationProperties(InstallerBootstrapProperties.class)
public class InstallerConfiguration {
@Bean
WebClient installerWebClient(InstallerProperties properties) {
return WebClient.builder().baseUrl(properties.cloudUrl()).build();
InstallerSettings installerSettings(
InstallerBootstrapProperties bootstrap,
IniConfigurationLoader loader) {
return loader.load(
Path.of(bootstrap.config()).toAbsolutePath().normalize(),
bootstrap.version());
}
@Bean
ProductProfile productProfile(
InstallerSettings settings,
ProductProfileRegistry registry) {
return registry.resolve(settings);
}
@Bean
WebClient installerWebClient(InstallerSettings settings) {
return WebClient.builder()
.baseUrl(settings.cloudServiceUrl().toString())
.build();
}
}

View File

@@ -0,0 +1,11 @@
package com.cygnus.installer;
public class InstallerConfigurationException extends RuntimeException {
public InstallerConfigurationException(String message) {
super(message);
}
public InstallerConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,9 +0,0 @@
package com.cygnus.installer;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "cygnus.installer")
public record InstallerProperties(
String cloudUrl,
String outputDirectory,
String version) {}

View File

@@ -0,0 +1,23 @@
package com.cygnus.installer;
import java.net.URI;
import java.nio.file.Path;
public record InstallerSettings(
String product,
URI cloudServiceUrl,
String installerApiBasePath,
String environment,
Path defaultOutputDirectory,
Path configurationDirectory,
String installerVersion,
IniDocument ini) {
public String validationPath() {
return installerApiBasePath + "/activation/validate";
}
public String registrationPath() {
return installerApiBasePath + "/register";
}
}

View File

@@ -0,0 +1,81 @@
package com.cygnus.installer;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
import org.springframework.stereotype.Service;
@Service
public class MachineAssertionService {
public String generate(
String clientId,
String installationCode,
String tokenAudience,
InstallationKeyPair installationKeys,
ProductProfile profile) {
try {
Instant issuedAt = Instant.now();
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.issuer(clientId)
.subject(clientId)
.audience(tokenAudience)
.issueTime(Date.from(issuedAt))
.expirationTime(Date.from(issuedAt.plus(365, ChronoUnit.DAYS)))
.claim("installation_id", installationCode)
.build();
SignedJWT signed = new SignedJWT(
new JWSHeader(JWSAlgorithm.RS256), claims);
signed.sign(new RSASSASigner(privateKey(installationKeys.privateKeyPem())));
JWEObject encrypted = new JWEObject(
new JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM),
new Payload(signed.serialize()));
encrypted.encrypt(new RSAEncrypter(publicKey(
Files.readString(
profile.assertionEncryptionPublicKey(),
StandardCharsets.US_ASCII))));
return encrypted.serialize();
} catch (Exception exception) {
throw new IllegalStateException(
"Could not generate the local machine assertion", exception);
}
}
private RSAPrivateKey privateKey(String pem) throws Exception {
return (RSAPrivateKey) KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(
Base64.getDecoder().decode(pem(pem, "PRIVATE KEY"))));
}
private RSAPublicKey publicKey(String pem) throws Exception {
return (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(
Base64.getDecoder().decode(pem(pem, "PUBLIC KEY"))));
}
private String pem(String value, String type) {
return value.replace("-----BEGIN " + type + "-----", "")
.replace("-----END " + type + "-----", "")
.replaceAll("\\s", "");
}
}

View File

@@ -0,0 +1,17 @@
package com.cygnus.installer;
import java.nio.file.Path;
import java.util.List;
public record ProductProfile(
String product,
String displayName,
String serviceName,
String imageEnvironmentVariable,
String configurationRoot,
String configurationFileName,
String installationConfigEnvironmentVariable,
String containerConfigurationPath,
String tokenPath,
Path assertionEncryptionPublicKey,
List<String> optionalInstallationFields) {}

View File

@@ -0,0 +1,97 @@
package com.cygnus.installer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import org.springframework.stereotype.Component;
@Component
public class ProductProfileRegistry {
public ProductProfile resolve(InstallerSettings settings) {
String section = "profile." + settings.product();
IniDocument ini = settings.ini();
Path publicKey = resolvePath(
settings.configurationDirectory(),
ini.required(section, "assertion_encryption_public_key"));
if (!Files.isRegularFile(publicKey)) {
throw new InstallerConfigurationException(
"Assertion encryption public key was not found: " + publicKey);
}
return new ProductProfile(
settings.product(),
ini.required(section, "display_name"),
safeIdentifier(
ini.required(section, "service_name"),
"service_name"),
safeIdentifier(
ini.required(section, "image_environment_variable"),
"image_environment_variable"),
safeIdentifier(
ini.required(section, "configuration_root"),
"configuration_root"),
safeFileName(
ini.required(section, "configuration_file_name"),
"configuration_file_name"),
safeIdentifier(
ini.required(
section,
"installation_config_environment_variable"),
"installation_config_environment_variable"),
absoluteContainerPath(
ini.required(section, "container_configuration_path")),
absoluteApiPath(ini.required(section, "token_path")),
publicKey,
commaSeparated(ini.optional(section, "optional_fields", "")));
}
private String safeIdentifier(String value, String setting) {
if (!value.matches("[A-Za-z][A-Za-z0-9_-]*")) {
throw new InstallerConfigurationException(setting + " contains invalid characters");
}
return value;
}
private String safeFileName(String value, String setting) {
if (!value.matches("[A-Za-z0-9][A-Za-z0-9._-]*")) {
throw new InstallerConfigurationException(setting + " must be a safe file name");
}
return value;
}
private String absoluteContainerPath(String value) {
if (!value.startsWith("/") || value.contains("..")) {
throw new InstallerConfigurationException(
"container_configuration_path must be an absolute safe path");
}
return value;
}
private String absoluteApiPath(String value) {
if (!value.startsWith("/") || value.contains("..")) {
throw new InstallerConfigurationException(
"token_path must be an absolute API path");
}
return value;
}
private List<String> commaSeparated(String value) {
if (value.isBlank()) {
return List.of();
}
return Arrays.stream(value.split(","))
.map(String::trim)
.map(field -> safeIdentifier(
field.toLowerCase(Locale.ROOT), "optional_fields"))
.toList();
}
private Path resolvePath(Path directory, String value) {
Path path = Path.of(value);
return (path.isAbsolute() ? path : directory.resolve(path))
.toAbsolutePath()
.normalize();
}
}

View File

@@ -0,0 +1,515 @@
package com.cygnus.installer;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import org.springframework.stereotype.Service;
@Service
public class SwingInstallationWizard {
private static final String WELCOME = "welcome";
private static final String DETAILS = "details";
private static final String INSTALLING = "installing";
private static final Color ACCENT = new Color(35, 97, 146);
private static final Color BACKGROUND = new Color(236, 243, 247);
private static final Color PANEL = new Color(249, 251, 252);
private final InstallationService installationService;
private final InstallerSettings settings;
private final ProductProfile profile;
private final CountDownLatch closed = new CountDownLatch(1);
private JFrame frame;
private CardLayout cards;
private JPanel cardPanel;
private JTextField clientCode;
private JPasswordField licenseKey;
private JTextField installationCode;
private JTextField installationName;
private JTextField dockerImage;
private JTextField outputDirectory;
private final Map<String, JTextField> optionalFields = new LinkedHashMap<>();
private JProgressBar progressBar;
private JLabel progressMessage;
private JTextArea resultText;
private JButton closeButton;
public SwingInstallationWizard(
InstallationService installationService,
InstallerSettings settings,
ProductProfile profile) {
this.installationService = installationService;
this.settings = settings;
this.profile = profile;
}
public void launchAndWait() {
configureLookAndFeel();
try {
SwingUtilities.invokeAndWait(this::createAndShow);
closed.await();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Installer was interrupted", exception);
} catch (Exception exception) {
throw new IllegalStateException("Could not start installer window", exception);
}
}
private void configureLookAndFeel() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {
// The cross-platform Swing look and feel remains available.
}
}
private void createAndShow() {
frame = new JFrame("Technobee Service Installer");
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
close();
}
});
frame.setMinimumSize(new Dimension(720, 540));
frame.setSize(760, 590);
frame.setLocationRelativeTo(null);
frame.setContentPane(rootPanel());
frame.setVisible(true);
}
private JPanel rootPanel() {
JPanel root = new JPanel(new BorderLayout());
root.setBackground(BACKGROUND);
root.add(header(), BorderLayout.NORTH);
cards = new CardLayout();
cardPanel = new JPanel(cards);
cardPanel.setBackground(BACKGROUND);
cardPanel.add(welcomePanel(), WELCOME);
cardPanel.add(detailsPanel(), DETAILS);
cardPanel.add(installingPanel(), INSTALLING);
root.add(cardPanel, BorderLayout.CENTER);
return root;
}
private JPanel header() {
JPanel header = new JPanel();
header.setBackground(ACCENT);
header.setBorder(BorderFactory.createEmptyBorder(18, 24, 18, 24));
header.setLayout(new BoxLayout(header, BoxLayout.Y_AXIS));
JLabel title = new JLabel("Technobee Service Installer");
title.setForeground(Color.WHITE);
title.setFont(title.getFont().deriveFont(Font.BOLD, 22f));
JLabel product = new JLabel(profile.displayName() + " installation");
product.setForeground(new Color(220, 235, 246));
product.setFont(product.getFont().deriveFont(13f));
header.add(title);
header.add(Box.createVerticalStrut(4));
header.add(product);
return header;
}
private JPanel welcomePanel() {
JPanel panel = contentPanel();
panel.setLayout(new BorderLayout(0, 18));
JPanel copy = new JPanel();
copy.setOpaque(false);
copy.setLayout(new BoxLayout(copy, BoxLayout.Y_AXIS));
copy.add(heading("Welcome"));
copy.add(Box.createVerticalStrut(10));
copy.add(text("This wizard validates your license, registers this server, "
+ "and creates the protected on-premises deployment package."));
copy.add(Box.createVerticalStrut(22));
copy.add(summaryRow("Product", profile.displayName()));
copy.add(summaryRow("Cloud service", settings.cloudServiceUrl().toString()));
copy.add(summaryRow("Environment", settings.environment()));
copy.add(summaryRow("Configuration", settings.configurationDirectory().toString()));
panel.add(copy, BorderLayout.CENTER);
JButton continueButton = primaryButton("Check Docker and continue");
continueButton.addActionListener(event -> checkPrerequisites(continueButton));
JPanel actions = actions(null, continueButton);
panel.add(actions, BorderLayout.SOUTH);
return panel;
}
private JPanel detailsPanel() {
JPanel panel = contentPanel();
panel.setLayout(new BorderLayout(0, 12));
JPanel top = new JPanel(new BorderLayout());
top.setOpaque(false);
top.add(heading("Installation details"), BorderLayout.NORTH);
top.add(text("Enter the activation details supplied by Technobee."), 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)));
clientCode = new JTextField();
licenseKey = new JPasswordField();
installationCode = new JTextField();
installationName = new JTextField();
dockerImage = new JTextField();
outputDirectory = new JTextField(
settings.defaultOutputDirectory().toAbsolutePath().normalize().toString());
int row = 0;
row = addField(form, row, "Client code", clientCode, null);
row = addField(form, row, "License key", licenseKey, null);
row = addField(form, row, "Installation code", installationCode, null);
row = addField(form, row, "Installation name", installationName, null);
row = addField(
form,
row,
profile.displayName() + " Docker image",
dockerImage,
null);
for (String field : profile.optionalInstallationFields()) {
JTextField input = new JTextField();
optionalFields.put(field, input);
row = addField(form, row, displayLabel(field), input, null);
}
JButton browse = secondaryButton("Browse…");
browse.addActionListener(event -> chooseOutputDirectory());
addField(form, row, "Output directory", outputDirectory, browse);
JScrollPane scroll = new JScrollPane(form);
scroll.setBorder(BorderFactory.createEmptyBorder());
scroll.getVerticalScrollBar().setUnitIncrement(12);
panel.add(scroll, BorderLayout.CENTER);
JButton back = secondaryButton("Back");
back.addActionListener(event -> cards.show(cardPanel, WELCOME));
JButton install = primaryButton("Install");
install.addActionListener(event -> startInstallation());
panel.add(actions(back, install), BorderLayout.SOUTH);
return panel;
}
private JPanel installingPanel() {
JPanel panel = contentPanel();
panel.setLayout(new BorderLayout(0, 18));
JPanel center = new JPanel();
center.setOpaque(false);
center.setLayout(new BoxLayout(center, BoxLayout.Y_AXIS));
center.add(heading("Installing " + profile.displayName()));
center.add(Box.createVerticalStrut(20));
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true);
progressBar.setAlignmentX(Component.LEFT_ALIGNMENT);
progressBar.setMaximumSize(new Dimension(Integer.MAX_VALUE, 28));
center.add(progressBar);
center.add(Box.createVerticalStrut(10));
progressMessage = text("Preparing installation");
center.add(progressMessage);
center.add(Box.createVerticalStrut(20));
resultText = new JTextArea(7, 40);
resultText.setEditable(false);
resultText.setLineWrap(true);
resultText.setWrapStyleWord(true);
resultText.setBackground(PANEL);
resultText.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
resultText.setVisible(false);
center.add(resultText);
panel.add(center, BorderLayout.CENTER);
closeButton = primaryButton("Close");
closeButton.setEnabled(false);
closeButton.addActionListener(event -> close());
panel.add(actions(null, closeButton), BorderLayout.SOUTH);
return panel;
}
private void checkPrerequisites(JButton button) {
button.setEnabled(false);
button.setText("Checking Docker…");
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() {
installationService.verifyPrerequisites();
return null;
}
@Override
protected void done() {
button.setEnabled(true);
button.setText("Check Docker and continue");
try {
get();
cards.show(cardPanel, DETAILS);
clientCode.requestFocusInWindow();
} catch (Exception exception) {
showError(message(exception));
}
}
}.execute();
}
private void startInstallation() {
char[] secret = licenseKey.getPassword();
InstallationRequest request;
try {
request = new InstallationRequest(
clientCode.getText(),
new String(secret),
installationCode.getText(),
installationName.getText(),
dockerImage.getText(),
Path.of(outputDirectory.getText().trim()),
optionalValues());
validateForm(request);
} catch (Exception exception) {
Arrays.fill(secret, '\0');
showError(exception.getMessage());
return;
}
Arrays.fill(secret, '\0');
cards.show(cardPanel, INSTALLING);
closeButton.setEnabled(false);
resultText.setVisible(false);
new SwingWorker<InstallationResult, ProgressUpdate>() {
@Override
protected InstallationResult doInBackground() {
return installationService.install(
request,
(percentage, message) ->
publish(new ProgressUpdate(percentage, message)));
}
@Override
protected void process(java.util.List<ProgressUpdate> updates) {
ProgressUpdate update = updates.getLast();
progressBar.setValue(update.percentage());
progressMessage.setText(update.message());
}
@Override
protected void done() {
closeButton.setEnabled(true);
try {
InstallationResult result = get();
progressBar.setValue(100);
progressMessage.setText("Installation completed");
resultText.setText(
"Installation ID: " + result.installationId()
+ "\nClient ID: " + result.clientId()
+ "\nOutput directory: " + result.outputDirectory()
+ "\n\nKeep the private key and machine assertion secure.");
resultText.setVisible(true);
resultText.revalidate();
} catch (Exception exception) {
progressMessage.setText("Installation failed");
progressBar.setValue(0);
resultText.setText(message(exception));
resultText.setForeground(new Color(160, 35, 35));
resultText.setVisible(true);
resultText.revalidate();
}
}
}.execute();
}
private void validateForm(InstallationRequest request) {
if (request.clientCode().isBlank()) {
throw new IllegalArgumentException("Client code is required.");
}
if (request.licenseKey().isBlank()) {
throw new IllegalArgumentException("License key is required.");
}
if (request.installationCode().isBlank()) {
throw new IllegalArgumentException("Installation code is required.");
}
if (request.installationName().isBlank()) {
throw new IllegalArgumentException("Installation name is required.");
}
if (request.dockerImage().isBlank()) {
throw new IllegalArgumentException("Docker image is required.");
}
if (outputDirectory.getText().isBlank()) {
throw new IllegalArgumentException("Output directory is required.");
}
}
private Map<String, String> optionalValues() {
Map<String, String> values = new LinkedHashMap<>();
optionalFields.forEach((name, input) -> values.put(name, input.getText().trim()));
return values;
}
private void chooseOutputDirectory() {
JFileChooser chooser = new JFileChooser(outputDirectory.getText());
chooser.setDialogTitle("Select installation output directory");
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
outputDirectory.setText(chooser.getSelectedFile().toPath()
.toAbsolutePath().normalize().toString());
}
}
private int addField(
JPanel form,
int row,
String label,
JTextField input,
JButton accessory) {
GridBagConstraints labelConstraints = constraints(0, row);
labelConstraints.weightx = 0;
labelConstraints.fill = GridBagConstraints.NONE;
labelConstraints.anchor = GridBagConstraints.LINE_START;
JLabel fieldLabel = new JLabel(label);
fieldLabel.setFont(fieldLabel.getFont().deriveFont(Font.BOLD, 12f));
form.add(fieldLabel, labelConstraints);
GridBagConstraints inputConstraints = constraints(1, row);
inputConstraints.weightx = 1;
inputConstraints.fill = GridBagConstraints.HORIZONTAL;
input.setPreferredSize(new Dimension(360, 29));
form.add(input, inputConstraints);
if (accessory != null) {
GridBagConstraints buttonConstraints = constraints(2, row);
buttonConstraints.weightx = 0;
buttonConstraints.fill = GridBagConstraints.NONE;
form.add(accessory, buttonConstraints);
}
return row + 1;
}
private GridBagConstraints constraints(int column, int row) {
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridx = column;
constraints.gridy = row;
constraints.insets = new Insets(6, column == 0 ? 0 : 12, 6, 0);
return constraints;
}
private JPanel contentPanel() {
JPanel panel = new JPanel();
panel.setBackground(BACKGROUND);
panel.setBorder(BorderFactory.createEmptyBorder(24, 30, 22, 30));
return panel;
}
private JPanel actions(JButton left, JButton right) {
JPanel actions = new JPanel(new BorderLayout());
actions.setOpaque(false);
actions.setBorder(BorderFactory.createCompoundBorder(
new JSeparator().getBorder(),
BorderFactory.createEmptyBorder(14, 0, 0, 0)));
if (left != null) {
actions.add(left, BorderLayout.WEST);
}
actions.add(right, BorderLayout.EAST);
return actions;
}
private JLabel heading(String value) {
JLabel label = new JLabel(value);
label.setFont(label.getFont().deriveFont(Font.BOLD, 19f));
label.setForeground(new Color(35, 48, 58));
return label;
}
private JLabel text(String value) {
JLabel label = new JLabel("<html>" + value + "</html>");
label.setForeground(new Color(75, 88, 99));
label.setAlignmentX(Component.LEFT_ALIGNMENT);
return label;
}
private JPanel summaryRow(String label, String value) {
JPanel row = new JPanel(new BorderLayout(18, 0));
row.setOpaque(false);
row.setBorder(BorderFactory.createEmptyBorder(7, 0, 7, 0));
JLabel name = new JLabel(label);
name.setPreferredSize(new Dimension(130, 20));
name.setFont(name.getFont().deriveFont(Font.BOLD));
row.add(name, BorderLayout.WEST);
row.add(new JLabel(value), BorderLayout.CENTER);
row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 36));
return row;
}
private JButton primaryButton(String text) {
JButton button = new JButton(text);
button.setBackground(ACCENT);
button.setForeground(Color.WHITE);
button.setOpaque(true);
button.setBorder(BorderFactory.createEmptyBorder(8, 18, 8, 18));
return button;
}
private JButton secondaryButton(String text) {
JButton button = new JButton(text);
button.setBorder(BorderFactory.createEmptyBorder(8, 18, 8, 18));
return button;
}
private String displayLabel(String field) {
String words = field.replace('_', ' ').replace('-', ' ');
return Character.toUpperCase(words.charAt(0)) + words.substring(1);
}
private void showError(String message) {
JOptionPane.showMessageDialog(
frame,
message,
"Technobee Service Installer",
JOptionPane.ERROR_MESSAGE);
}
private String message(Throwable throwable) {
Throwable current = throwable;
while (current.getCause() != null) {
current = current.getCause();
}
return current.getMessage() == null
? current.getClass().getSimpleName()
: current.getMessage();
}
private void close() {
if (frame != null) {
frame.dispose();
}
closed.countDown();
}
private record ProgressUpdate(int percentage, String message) {}
}

View File

@@ -0,0 +1,68 @@
package com.cygnus.installer;
import java.awt.GraphicsEnvironment;
import java.util.Arrays;
import javax.swing.JOptionPane;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class TechnobeeServiceInstallerApplication implements CommandLineRunner {
private final InstallationWizard wizard;
private final SwingInstallationWizard swingWizard;
public TechnobeeServiceInstallerApplication(
InstallationWizard wizard,
SwingInstallationWizard swingWizard) {
this.wizard = wizard;
this.swingWizard = swingWizard;
}
public static void main(String[] args) {
SpringApplication application =
new SpringApplication(TechnobeeServiceInstallerApplication.class);
application.setWebApplicationType(WebApplicationType.NONE);
application.setLogStartupInfo(false);
try {
ConfigurableApplicationContext context = application.run(args);
System.exit(SpringApplication.exit(context));
} catch (Exception exception) {
showStartupError(exception, args);
System.exit(1);
}
}
@Override
public void run(String... args) {
boolean cli = Arrays.asList(args).contains("--cli");
if (cli || GraphicsEnvironment.isHeadless()) {
wizard.run();
return;
}
swingWizard.launchAndWait();
}
private static void showStartupError(Exception exception, String[] args) {
Throwable cause = exception;
while (cause.getCause() != null) {
cause = cause.getCause();
}
String message = cause.getMessage() == null
? cause.getClass().getSimpleName()
: cause.getMessage();
if (!GraphicsEnvironment.isHeadless()
&& !Arrays.asList(args).contains("--cli")) {
JOptionPane.showMessageDialog(
null,
message,
"Technobee Service Installer configuration error",
JOptionPane.ERROR_MESSAGE);
} else {
System.err.println("Technobee Service Installer: " + message);
}
}
}

View File

@@ -2,12 +2,11 @@ spring:
main:
banner-mode: "off"
application:
name: cygnus-installer
cygnus:
name: technobee-service-installer
technobee:
installer:
cloud-url: ${CYGNUS_CLOUD_URL:http://localhost:8090}
output-directory: ${CYGNUS_INSTALL_OUTPUT:./cygnus-installation}
version: ${CYGNUS_INSTALLER_VERSION:1.0.0}
config: ${TECHNOBEE_INSTALLER_CONFIG:./installer.ini}
version: ${TECHNOBEE_INSTALLER_VERSION:1.0.0}
logging:
level:
root: WARN

View File

@@ -0,0 +1,95 @@
package com.cygnus.installer;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class DeploymentWriterTest {
@TempDir
Path temporaryDirectory;
@Test
void writesPortableProductDeploymentAndKeepsSecretsLocal() throws Exception {
UUID tenantId = UUID.randomUUID();
UUID installationId = UUID.randomUUID();
UUID installationUuid = UUID.randomUUID();
var validation = new ActivationDtos.ValidationResponse(
"activation-token",
OffsetDateTime.now().plusMinutes(5),
tenantId,
"matrix-client",
"FULL",
2);
var registration = new ActivationDtos.RegistrationResponse(
installationId,
installationUuid,
tenantId,
null,
"primary",
1,
"ACTIVE");
var profile = new ProductProfile(
"matrix",
"Matrix",
"matrix-onprem",
"MATRIX_IMAGE",
"matrix",
"installation.yml",
"MATRIX_INSTALLATION_CONFIG",
"/srv/matrix/config/installation.yml",
"/oauth2/token",
temporaryDirectory.resolve("unused.pem"),
List.of("region"));
var settings = new InstallerSettings(
"matrix",
URI.create("https://cloud.example.com"),
"/api/v1/installations",
"production",
temporaryDirectory,
temporaryDirectory,
"1",
new IniDocument(Map.of()));
Path output = new DeploymentWriter().write(
temporaryDirectory.resolve("output"),
installationUuid,
"primary",
"matrix-client",
validation,
registration,
new InstallationKeyService().generate(),
"encrypted.assertion.value",
settings,
profile,
"registry.example.com/matrix:1.0.0",
Map.of("region", "north"));
String config = Files.readString(output.resolve("config/installation.yml"));
String compose = Files.readString(output.resolve("compose.yml"));
assertThat(config)
.contains("client-id: \"matrix-client\"")
.contains("installation-id: \"" + installationId + "\"")
.contains("cloud-url: \"https://cloud.example.com\"")
.contains("region: \"north\"")
.doesNotContain("activation-token");
assertThat(compose)
.contains("matrix-onprem:")
.contains("./config:/srv/matrix/config:ro")
.contains("MATRIX_INSTALLATION_CONFIG");
assertThat(output.resolve(".env"))
.hasContent("MATRIX_IMAGE=registry.example.com/matrix:1.0.0\n");
assertThat(output.resolve("config/machine-assertion.jwt")).hasContent(
"encrypted.assertion.value");
assertThat(output.resolve("config/keys/client-signing-private.pem"))
.isRegularFile();
}
}

View File

@@ -2,9 +2,6 @@ package com.cygnus.installer;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.time.Duration;
import java.util.List;
@@ -14,19 +11,24 @@ class DockerPrerequisiteCheckerTest {
@Test
void blocksInstallationWhenDockerIsUnavailable() {
CommandExecutor executor = mock(CommandExecutor.class);
when(executor.execute(any(List.class), any(Duration.class)))
.thenReturn(new CommandResult(127, "not found"));
CommandExecutor executor = executorReturning(new CommandResult(127, "not found"));
DockerPrerequisiteChecker checker = new DockerPrerequisiteChecker(executor);
assertThrows(PrerequisiteException.class, checker::verify);
}
@Test
void acceptsDockerEngineAndComposeV2() {
CommandExecutor executor = mock(CommandExecutor.class);
when(executor.execute(any(List.class), any(Duration.class)))
.thenReturn(new CommandResult(0, "ok"));
CommandExecutor executor = executorReturning(new CommandResult(0, "ok"));
DockerPrerequisiteChecker checker = new DockerPrerequisiteChecker(executor);
assertDoesNotThrow(checker::verify);
}
private CommandExecutor executorReturning(CommandResult result) {
return new CommandExecutor() {
@Override
public CommandResult execute(List<String> command, Duration timeout) {
return result;
}
};
}
}

View File

@@ -0,0 +1,84 @@
package com.cygnus.installer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class IniConfigurationLoaderTest {
@TempDir
Path temporaryDirectory;
private final IniConfigurationLoader loader = new IniConfigurationLoader();
@Test
void loadsAndNormalizesInstallerSettings() throws Exception {
Path ini = write("""
[installer]
product=matrix
cloud_service_url=https://cloud.example.com/
installer_api_base_path=/api/v1/installations/
environment=production
""");
InstallerSettings settings = loader.load(ini, "1.2.3");
assertThat(settings.product()).isEqualTo("matrix");
assertThat(settings.cloudServiceUrl().toString())
.isEqualTo("https://cloud.example.com");
assertThat(settings.validationPath())
.isEqualTo("/api/v1/installations/activation/validate");
assertThat(settings.registrationPath())
.isEqualTo("/api/v1/installations/register");
assertThat(settings.defaultOutputDirectory())
.isEqualTo(Path.of("./matrix-installation"));
}
@Test
void rejectsUnknownProduct() throws Exception {
Path ini = write("""
[installer]
product=unknown
cloud_service_url=https://cloud.example.com
installer_api_base_path=/api/v1/installations
environment=production
""");
assertThatThrownBy(() -> loader.load(ini, "1"))
.isInstanceOf(InstallerConfigurationException.class)
.hasMessageContaining("Unsupported product");
}
@Test
void rejectsInvalidCloudUrlAndApiPath() throws Exception {
Path invalidUrl = write("""
[installer]
product=cygnus
cloud_service_url=cloud.example.com
installer_api_base_path=/api/v1/installations
environment=production
""");
assertThatThrownBy(() -> loader.load(invalidUrl, "1"))
.hasMessageContaining("absolute http(s) URL");
Path invalidPath = write("""
[installer]
product=cygnus
cloud_service_url=https://cloud.example.com
installer_api_base_path=api/v1/installations
environment=production
""");
assertThatThrownBy(() -> loader.load(invalidPath, "1"))
.hasMessageContaining("absolute safe URL path");
}
private Path write(String content) throws Exception {
Path path = temporaryDirectory.resolve("installer-" + System.nanoTime() + ".ini");
Files.writeString(path, content);
return path;
}
}

View File

@@ -0,0 +1,29 @@
package com.cygnus.installer;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.nio.file.Path;
import java.util.Map;
import org.junit.jupiter.api.Test;
class InstallationServiceTest {
@Test
void rejectsIncompleteRequestBeforeCallingInfrastructure() {
InstallationService service =
new InstallationService(null, null, null, null, null, null, null);
assertThatThrownBy(() -> service.install(
new InstallationRequest(
"",
"",
"",
"",
"",
Path.of("."),
Map.of()),
(percentage, message) -> {}))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Client code is required");
}
}

View File

@@ -0,0 +1,56 @@
package com.cygnus.installer;
import static org.assertj.core.api.Assertions.assertThat;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPairGenerator;
import java.util.Base64;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class MachineAssertionServiceTest {
@TempDir
Path temporaryDirectory;
@Test
void generatesEncryptedAssertionUsingOnlyCloudPublicKey() throws Exception {
var generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
var cloudKeys = generator.generateKeyPair();
Path cloudPublicKey = temporaryDirectory.resolve("cloud-public.pem");
Files.writeString(cloudPublicKey, pem(
"PUBLIC KEY", cloudKeys.getPublic().getEncoded()));
ProductProfile profile = new ProductProfile(
"matrix",
"Matrix",
"matrix-onprem",
"MATRIX_IMAGE",
"matrix",
"installation.yml",
"MATRIX_INSTALLATION_CONFIG",
"/opt/matrix/config/installation.yml",
"/oauth2/token",
cloudPublicKey,
List.of());
String assertion = new MachineAssertionService().generate(
"matrix-client",
"primary",
"https://cloud.example.com/oauth2/token",
new InstallationKeyService().generate(),
profile);
assertThat(assertion.split("\\.")).hasSize(5);
assertThat(assertion).doesNotContain("matrix-client");
}
private String pem(String type, byte[] encoded) {
return "-----BEGIN " + type + "-----\n"
+ Base64.getMimeEncoder(64, new byte[] {'\n'})
.encodeToString(encoded)
+ "\n-----END " + type + "-----\n";
}
}

View File

@@ -0,0 +1,49 @@
package com.cygnus.installer;
import static org.assertj.core.api.Assertions.assertThat;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class ProductProfileRegistryTest {
@TempDir
Path temporaryDirectory;
@Test
void appliesProductProfileOverridesWithoutInstallerBranching() throws Exception {
Path publicKey = temporaryDirectory.resolve("cloud.pem");
Files.writeString(publicKey, "test");
Path ini = temporaryDirectory.resolve("installer.ini");
Files.writeString(ini, """
[installer]
product=cygnus
cloud_service_url=https://cloud.example.com
installer_api_base_path=/api/v1/installations
environment=production
[profile.cygnus]
display_name=Cygnus
assertion_encryption_public_key=cloud.pem
service_name=custom-cygnus
image_environment_variable=CYGNUS_IMAGE
configuration_root=cygnus
configuration_file_name=installation.yml
installation_config_environment_variable=CYGNUS_INSTALLATION_CONFIG
container_configuration_path=/opt/cygnus/config/installation.yml
token_path=/oauth2/token
optional_fields=region,site_code
""");
InstallerSettings settings = new IniConfigurationLoader().load(ini, "1");
ProductProfile profile = new ProductProfileRegistry().resolve(settings);
assertThat(profile.displayName()).isEqualTo("Cygnus");
assertThat(profile.serviceName()).isEqualTo("custom-cygnus");
assertThat(profile.optionalInstallationFields())
.containsExactly("region", "site_code");
assertThat(profile.assertionEncryptionPublicKey()).isEqualTo(publicKey);
}
}

View File

@@ -2,12 +2,11 @@ spring:
main:
banner-mode: "off"
application:
name: cygnus-installer
cygnus:
name: technobee-service-installer
technobee:
installer:
cloud-url: ${CYGNUS_CLOUD_URL:http://localhost:8090}
output-directory: ${CYGNUS_INSTALL_OUTPUT:./cygnus-installation}
version: ${CYGNUS_INSTALLER_VERSION:1.0.0}
config: ${TECHNOBEE_INSTALLER_CONFIG:./installer.ini}
version: ${TECHNOBEE_INSTALLER_VERSION:1.0.0}
logging:
level:
root: WARN

View File

@@ -1,3 +1,3 @@
artifactId=cygnus-installer
artifactId=technobee-service-installer
groupId=com.cygnus
version=1.0.0-SNAPSHOT

View File

@@ -1,17 +1,33 @@
com/cygnus/installer/MachineAssertionService.class
com/cygnus/installer/ActivationDtos$ValidationResponse.class
com/cygnus/installer/ActivationDtos.class
com/cygnus/installer/DockerPrerequisiteChecker.class
com/cygnus/installer/SwingInstallationWizard$2.class
com/cygnus/installer/CommandResult.class
com/cygnus/installer/InstallationProgressListener.class
com/cygnus/installer/ProductProfileRegistry.class
com/cygnus/installer/InstallationRequest.class
com/cygnus/installer/InstallerConfiguration.class
com/cygnus/installer/CygnusInstallerApplication.class
com/cygnus/installer/ProductProfile.class
com/cygnus/installer/InstallationWizard.class
com/cygnus/installer/CommandExecutor.class
com/cygnus/installer/TechnobeeServiceInstallerApplication.class
com/cygnus/installer/DeploymentWriter.class
com/cygnus/installer/InstallerProperties.class
com/cygnus/installer/InstallationKeyService.class
com/cygnus/installer/ActivationClient.class
com/cygnus/installer/ActivationDtos$RegistrationRequest.class
com/cygnus/installer/ActivationDtos$RegistrationResponse.class
com/cygnus/installer/IniDocument.class
com/cygnus/installer/SwingInstallationWizard$3.class
com/cygnus/installer/InstallerConfigurationException.class
com/cygnus/installer/SwingInstallationWizard.class
com/cygnus/installer/ActivationDtos.class
com/cygnus/installer/DockerPrerequisiteChecker.class
com/cygnus/installer/InstallerBootstrapProperties.class
com/cygnus/installer/SwingInstallationWizard$1.class
com/cygnus/installer/CommandExecutor.class
com/cygnus/installer/IniConfigurationLoader.class
com/cygnus/installer/InstallationService.class
com/cygnus/installer/InstallationResult.class
com/cygnus/installer/ActivationDtos$RegistrationRequest.class
com/cygnus/installer/ActivationDtos$ValidationRequest.class
com/cygnus/installer/PrerequisiteException.class
com/cygnus/installer/InstallerSettings.class
com/cygnus/installer/InstallationKeyPair.class
com/cygnus/installer/SwingInstallationWizard$ProgressUpdate.class

View File

@@ -1,13 +1,25 @@
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ActivationClient.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ActivationDtos.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CommandExecutor.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CommandResult.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CygnusInstallerApplication.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DeploymentWriter.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DockerPrerequisiteChecker.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationKeyPair.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationKeyService.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationWizard.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerConfiguration.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerProperties.java
/Users/maddy/Projects/matrix/cygnus-installer/src/main/java/com/cygnus/installer/PrerequisiteException.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ActivationClient.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ActivationDtos.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/DeploymentWriter.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/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/InstallationKeyPair.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationKeyService.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationProgressListener.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationRequest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationResult.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationService.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationWizard.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerBootstrapProperties.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerConfiguration.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerConfigurationException.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerSettings.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/MachineAssertionService.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/ProductProfileRegistry.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

View File

@@ -1 +1,7 @@
com/cygnus/installer/MachineAssertionServiceTest.class
com/cygnus/installer/InstallationServiceTest.class
com/cygnus/installer/DockerPrerequisiteCheckerTest$1.class
com/cygnus/installer/ProductProfileRegistryTest.class
com/cygnus/installer/IniConfigurationLoaderTest.class
com/cygnus/installer/DeploymentWriterTest.class
com/cygnus/installer/DockerPrerequisiteCheckerTest.class

View File

@@ -1 +1,6 @@
/Users/maddy/Projects/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DockerPrerequisiteCheckerTest.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/DockerPrerequisiteCheckerTest.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/MachineAssertionServiceTest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/ProductProfileRegistryTest.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

View File

@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
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

View File

@@ -1,4 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.DockerPrerequisiteCheckerTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.412 s -- in com.cygnus.installer.DockerPrerequisiteCheckerTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s -- in com.cygnus.installer.DockerPrerequisiteCheckerTest

View File

@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
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

View File

@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.InstallationServiceTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in com.cygnus.installer.InstallationServiceTest

View File

@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
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

View File

@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
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