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 One Swing-based Java installer supports Matrix and Cygnus through an external
3072-bit installation signing key, registers the installation, and writes the INI product profile. It never connects to PostgreSQL or Redis. The selected
deployment files. The license key is never persisted. product cloud service validates the activation key and registers the
installation.
## Build and run
```bash ```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 Use `config/cygnus-installer.ini` for Cygnus. Before production use, set the
files should be written and offers `CYGNUS_INSTALL_OUTPUT` as the default. 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 ```text
.env
compose.yml compose.yml
config/ config/
installation.yml installation.yml
machine-assertion.jwt
keys/ keys/
client-signing-private.pem client-signing-private.pem
client-signing-public.pem client-signing-public.pem
``` ```
The private key is created with owner-only permissions on POSIX systems. The The installer asks for the product Docker image, validates it, and stores it
plaintext installation license key is never written to the output directory. 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> <version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>
<artifactId>cygnus-installer</artifactId> <artifactId>technobee-service-installer</artifactId>
<name>Cygnus Installation Wizard</name> <name>Technobee Service Installer</name>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -27,6 +27,11 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-boot-starter-webflux</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux-test</artifactId> <artifactId>spring-boot-starter-webflux-test</artifactId>

View File

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

View File

@@ -30,8 +30,10 @@ final class ActivationDtos {
record RegistrationResponse( record RegistrationResponse(
UUID installationId, UUID installationId,
UUID installationUuid,
UUID tenantId, UUID tenantId,
UUID clientId, String clientId,
String installationCode, String installationCode,
int securityVersion,
String status) {} 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; package com.cygnus.installer;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermissions; import java.nio.file.attribute.PosixFilePermissions;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -14,55 +17,161 @@ public class DeploymentWriter {
Path output, Path output,
UUID installationUuid, UUID installationUuid,
String installationCode, String installationCode,
String clientId,
ActivationDtos.ValidationResponse validation, ActivationDtos.ValidationResponse validation,
ActivationDtos.RegistrationResponse registration, ActivationDtos.RegistrationResponse registration,
InstallationKeyPair keys, InstallationKeyPair keys,
String cloudUrl) { String machineAssertion,
InstallerSettings settings,
ProductProfile profile,
String dockerImage,
Map<String, String> optionalFields) {
try { try {
Path config = output.resolve("config"); Path normalizedOutput = output.toAbsolutePath().normalize();
Path config = normalizedOutput.resolve("config");
Path keyDirectory = config.resolve("keys"); Path keyDirectory = config.resolve("keys");
Files.createDirectories(keyDirectory); Files.createDirectories(keyDirectory);
writePrivate(keyDirectory.resolve("client-signing-private.pem"), keys.privateKeyPem());
Files.writeString( String privateRelative = "config/keys/client-signing-private.pem";
keyDirectory.resolve("client-signing-public.pem"), keys.publicKeyPem()); String publicRelative = "config/keys/client-signing-public.pem";
Files.writeString(config.resolve("installation.yml"), """ String assertionRelative = "config/machine-assertion.jwt";
cygnus: writePrivate(
cloud-url: "%s" normalizedOutput.resolve(privateRelative),
tenant-id: "%s" keys.privateKeyPem());
client-id: "%s" writePublic(
installation-id: "%s" normalizedOutput.resolve(publicRelative),
installation-uuid: "%s" keys.publicKeyPem());
installation-code: "%s" writePrivate(
signing-private-key: "file:./config/keys/client-signing-private.pem" normalizedOutput.resolve(assertionRelative),
""".formatted( machineAssertion);
cloudUrl, writePublic(
validation.tenantId(), config.resolve(profile.configurationFileName()),
registration.clientId(), installationConfiguration(
registration.installationId(), installationUuid,
installationUuid, installationCode,
installationCode)); clientId,
Files.writeString(output.resolve("compose.yml"), """ validation,
services: registration,
cygnus-onprem: settings,
image: ${CYGNUS_IMAGE:?Set CYGNUS_IMAGE} profile,
restart: unless-stopped privateRelative,
volumes: publicRelative,
- ./config:/opt/cygnus/config:ro assertionRelative,
environment: optionalFields));
CYGNUS_INSTALLATION_CONFIG: /opt/cygnus/config/installation.yml writePublic(
"""); normalizedOutput.resolve("compose.yml"),
return output; compose(profile));
} catch (IOException e) { writePrivate(
throw new IllegalStateException("Could not write installation files", e); 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 { 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 { try {
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rw-------")); Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rw-------"));
} catch (UnsupportedOperationException ignored) { } 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.io.Console;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
import java.util.UUID;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
public class InstallationWizard { public class InstallationWizard {
private final DockerPrerequisiteChecker prerequisites; private final InstallationService installationService;
private final ActivationClient activationClient; private final InstallerSettings settings;
private final InstallationKeyService keyService; private final ProductProfile profile;
private final DeploymentWriter deploymentWriter;
private final InstallerProperties properties;
public InstallationWizard( public InstallationWizard(
DockerPrerequisiteChecker prerequisites, InstallationService installationService,
ActivationClient activationClient, InstallerSettings settings,
InstallationKeyService keyService, ProductProfile profile) {
DeploymentWriter deploymentWriter, this.installationService = installationService;
InstallerProperties properties) { this.settings = settings;
this.prerequisites = prerequisites; this.profile = profile;
this.activationClient = activationClient;
this.keyService = keyService;
this.deploymentWriter = deploymentWriter;
this.properties = properties;
} }
public void run() { public void run() {
System.out.println("Cygnus installation wizard"); System.out.println("Technobee Service Installer");
prerequisites.verify(); System.out.println("Selected product: " + profile.displayName());
installationService.verifyPrerequisites();
System.out.println("Docker prerequisites: OK"); System.out.println("Docker prerequisites: OK");
Console console = System.console(); Console console = System.console();
@@ -39,34 +35,44 @@ public class InstallationWizard {
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");
String installationName = prompt(console, scanner, "Installation name"); 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( String outputDirectory = promptWithDefault(
console, console,
scanner, scanner,
"Output directory", "Output directory",
properties.outputDirectory()); settings.defaultOutputDirectory().toString());
UUID installationUuid = UUID.randomUUID(); InstallationResult result = installationService.install(
var validation = activationClient.validate( new InstallationRequest(
clientCode, licenseKey, installationUuid, properties.version()); clientCode,
var keys = keyService.generate(); licenseKey,
var registration = activationClient.register(
validation,
installationCode, installationCode,
installationName, installationName,
keys.publicKeyPem(), dockerImage,
properties.version(),
environment);
Path output = deploymentWriter.write(
Path.of(outputDirectory).toAbsolutePath().normalize(), Path.of(outputDirectory).toAbsolutePath().normalize(),
installationUuid, optionalFields),
installationCode, (percentage, message) ->
validation, System.out.println("[" + percentage + "%] " + message));
registration, System.out.println("Installation registered: " + result.installationId());
keys, System.out.println("Deployment files created at: " + result.outputDirectory());
properties.cloudUrl()); }
System.out.println("Installation registered: " + registration.installationId());
System.out.println("Deployment files created at: " + output); 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) { 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; package com.cygnus.installer;
import java.nio.file.Path;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
@Configuration @Configuration
@EnableConfigurationProperties(InstallerProperties.class) @EnableConfigurationProperties(InstallerBootstrapProperties.class)
public class InstallerConfiguration { public class InstallerConfiguration {
@Bean @Bean
WebClient installerWebClient(InstallerProperties properties) { InstallerSettings installerSettings(
return WebClient.builder().baseUrl(properties.cloudUrl()).build(); 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();
} }
} }

Some files were not shown because too many files have changed in this diff Show More