Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
- Implemented CreateInstanceView for creating new instances. - Added HostsView for managing SSH hosts with CRUD operations. - Created InstancesView for displaying and managing instances. - Developed LogsView for viewing operation logs. - Introduced SecretsView for managing secrets associated with hosts. - Established SettingsView for configuring application settings. - Created MainWindow as the main application window with navigation. - Added app manifest and configuration files for logging and settings.
245 lines
11 KiB
C#
245 lines
11 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Security.Cryptography;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using OTSSignsOrchestrator.Core.Data;
|
|
using OTSSignsOrchestrator.Core.Models.DTOs;
|
|
using OTSSignsOrchestrator.Core.Models.Entities;
|
|
using OTSSignsOrchestrator.Core.Services;
|
|
using OTSSignsOrchestrator.Desktop.Services;
|
|
|
|
namespace OTSSignsOrchestrator.Desktop.ViewModels;
|
|
|
|
/// <summary>
|
|
/// ViewModel for the Create Instance form.
|
|
/// Simplified: Customer Name, Abbreviation, SSH Host, and optional Newt credentials.
|
|
/// All other config comes from the Settings page.
|
|
/// </summary>
|
|
public partial class CreateInstanceViewModel : ObservableObject
|
|
{
|
|
private readonly IServiceProvider _services;
|
|
|
|
[ObservableProperty] private string _statusMessage = string.Empty;
|
|
[ObservableProperty] private bool _isBusy;
|
|
[ObservableProperty] private string _deployOutput = string.Empty;
|
|
[ObservableProperty] private double _progressPercent;
|
|
[ObservableProperty] private string _progressStep = string.Empty;
|
|
|
|
// Core form fields — only these two are required from the user
|
|
[ObservableProperty] private string _customerName = string.Empty;
|
|
[ObservableProperty] private string _customerAbbrev = string.Empty;
|
|
|
|
// Optional Pangolin/Newt credentials (per-instance)
|
|
[ObservableProperty] private string _newtId = string.Empty;
|
|
[ObservableProperty] private string _newtSecret = string.Empty;
|
|
|
|
// CIFS / SMB credentials (per-instance, defaults loaded from global settings)
|
|
[ObservableProperty] private string _cifsServer = string.Empty;
|
|
[ObservableProperty] private string _cifsShareBasePath = string.Empty;
|
|
[ObservableProperty] private string _cifsUsername = string.Empty;
|
|
[ObservableProperty] private string _cifsPassword = string.Empty;
|
|
[ObservableProperty] private string _cifsExtraOptions = string.Empty;
|
|
|
|
// SSH host selection
|
|
[ObservableProperty] private ObservableCollection<SshHost> _availableHosts = new();
|
|
[ObservableProperty] private SshHost? _selectedSshHost;
|
|
|
|
// ── Derived preview properties ───────────────────────────────────────────
|
|
|
|
public string PreviewStackName => Valid ? $"{Abbrev}-cms-stack" : "—";
|
|
public string PreviewServiceWeb => Valid ? $"{Abbrev}-web" : "—";
|
|
public string PreviewServiceCache => Valid ? $"{Abbrev}-memcached" : "—";
|
|
public string PreviewServiceChart => Valid ? $"{Abbrev}-quickchart" : "—";
|
|
public string PreviewServiceNewt => Valid ? $"{Abbrev}-newt" : "—";
|
|
public string PreviewNetwork => Valid ? $"{Abbrev}-net" : "—";
|
|
public string PreviewVolCustom => Valid ? $"{Abbrev}-cms-custom" : "—";
|
|
public string PreviewVolBackup => Valid ? $"{Abbrev}-cms-backup" : "—";
|
|
public string PreviewVolLibrary => Valid ? $"{Abbrev}-cms-library" : "—";
|
|
public string PreviewVolUserscripts => Valid ? $"{Abbrev}-cms-userscripts": "—";
|
|
public string PreviewVolCaCerts => Valid ? $"{Abbrev}-cms-ca-certs" : "—";
|
|
public string PreviewSecret => Valid ? $"{Abbrev}-cms-db-password": "—";
|
|
public string PreviewMySqlDb => Valid ? $"{Abbrev}_cms_db" : "—";
|
|
public string PreviewMySqlUser => Valid ? $"{Abbrev}_cms" : "—";
|
|
public string PreviewCmsUrl => Valid ? $"https://{Abbrev}.ots-signs.com" : "—";
|
|
|
|
private string Abbrev => CustomerAbbrev.Trim().ToLowerInvariant();
|
|
private bool Valid => Abbrev.Length == 3 && System.Text.RegularExpressions.Regex.IsMatch(Abbrev, "^[a-z]{3}$");
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public CreateInstanceViewModel(IServiceProvider services)
|
|
{
|
|
_services = services;
|
|
_ = LoadHostsAsync();
|
|
_ = LoadCifsDefaultsAsync();
|
|
}
|
|
|
|
partial void OnCustomerAbbrevChanged(string value) => RefreshPreview();
|
|
|
|
private void RefreshPreview()
|
|
{
|
|
OnPropertyChanged(nameof(PreviewStackName));
|
|
OnPropertyChanged(nameof(PreviewServiceWeb));
|
|
OnPropertyChanged(nameof(PreviewServiceCache));
|
|
OnPropertyChanged(nameof(PreviewServiceChart));
|
|
OnPropertyChanged(nameof(PreviewServiceNewt));
|
|
OnPropertyChanged(nameof(PreviewNetwork));
|
|
OnPropertyChanged(nameof(PreviewVolCustom));
|
|
OnPropertyChanged(nameof(PreviewVolBackup));
|
|
OnPropertyChanged(nameof(PreviewVolLibrary));
|
|
OnPropertyChanged(nameof(PreviewVolUserscripts));
|
|
OnPropertyChanged(nameof(PreviewVolCaCerts));
|
|
OnPropertyChanged(nameof(PreviewSecret));
|
|
OnPropertyChanged(nameof(PreviewMySqlDb));
|
|
OnPropertyChanged(nameof(PreviewMySqlUser));
|
|
OnPropertyChanged(nameof(PreviewCmsUrl));
|
|
}
|
|
|
|
private async Task LoadHostsAsync()
|
|
{
|
|
using var scope = _services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<XiboContext>();
|
|
var hosts = await db.SshHosts.OrderBy(h => h.Label).ToListAsync();
|
|
AvailableHosts = new ObservableCollection<SshHost>(hosts);
|
|
}
|
|
|
|
private async Task LoadCifsDefaultsAsync()
|
|
{
|
|
using var scope = _services.CreateScope();
|
|
var settings = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
|
CifsServer = await settings.GetAsync(SettingsService.CifsServer) ?? string.Empty;
|
|
CifsShareBasePath = await settings.GetAsync(SettingsService.CifsShareBasePath) ?? string.Empty;
|
|
CifsUsername = await settings.GetAsync(SettingsService.CifsUsername) ?? string.Empty;
|
|
CifsPassword = await settings.GetAsync(SettingsService.CifsPassword) ?? string.Empty;
|
|
CifsExtraOptions = await settings.GetAsync(SettingsService.CifsOptions, "file_mode=0777,dir_mode=0777");
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task DeployAsync()
|
|
{
|
|
// ── Validation ───────────────────────────────────────────────────
|
|
if (SelectedSshHost == null)
|
|
{
|
|
StatusMessage = "Select an SSH host first.";
|
|
return;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(CustomerName))
|
|
{
|
|
StatusMessage = "Customer Name is required.";
|
|
return;
|
|
}
|
|
if (!Valid)
|
|
{
|
|
StatusMessage = "Abbreviation must be exactly 3 lowercase letters (a-z).";
|
|
return;
|
|
}
|
|
|
|
IsBusy = true;
|
|
StatusMessage = "Starting deployment...";
|
|
DeployOutput = string.Empty;
|
|
ProgressPercent = 0;
|
|
|
|
try
|
|
{
|
|
// Wire SSH host into docker services
|
|
var dockerCli = _services.GetRequiredService<SshDockerCliService>();
|
|
dockerCli.SetHost(SelectedSshHost);
|
|
var dockerSecrets = _services.GetRequiredService<SshDockerSecretsService>();
|
|
dockerSecrets.SetHost(SelectedSshHost);
|
|
var ssh = _services.GetRequiredService<SshConnectionService>();
|
|
|
|
using var scope = _services.CreateScope();
|
|
var instanceSvc = scope.ServiceProvider.GetRequiredService<InstanceService>();
|
|
|
|
// ── Step 1: Clone template repo ────────────────────────────────
|
|
SetProgress(10, "Cloning template repository...");
|
|
// Handled inside InstanceService.CreateInstanceAsync
|
|
|
|
// ── Step 2: Generate MySQL password → Docker secret ────────────
|
|
SetProgress(20, "Generating secrets...");
|
|
var mysqlPassword = GenerateRandomPassword(32);
|
|
|
|
// ── Step 3: Create MySQL database + user via SSH ───────────────
|
|
SetProgress(35, "Creating MySQL database and user...");
|
|
var (mysqlOk, mysqlMsg) = await instanceSvc.CreateMySqlDatabaseAsync(
|
|
Abbrev,
|
|
mysqlPassword,
|
|
cmd => ssh.RunCommandAsync(SelectedSshHost, cmd));
|
|
|
|
AppendOutput($"[MySQL] {mysqlMsg}");
|
|
if (!mysqlOk)
|
|
{
|
|
StatusMessage = mysqlMsg;
|
|
return;
|
|
}
|
|
|
|
// ── Step 4: Create Docker Swarm secret ────────────────────────
|
|
SetProgress(50, "Creating Docker Swarm secrets...");
|
|
var secretName = $"{Abbrev}-cms-db-password";
|
|
var (_, secretId) = await dockerSecrets.EnsureSecretAsync(secretName, mysqlPassword);
|
|
AppendOutput($"[Secret] {secretName} → {secretId}");
|
|
|
|
// Password is now ONLY on the Swarm — clear from memory
|
|
mysqlPassword = string.Empty;
|
|
|
|
// ── Step 5: Deploy stack ──────────────────────────────────────
|
|
SetProgress(70, "Rendering compose & deploying stack...");
|
|
|
|
var dto = new CreateInstanceDto
|
|
{
|
|
CustomerName = CustomerName.Trim(),
|
|
CustomerAbbrev = Abbrev,
|
|
SshHostId = SelectedSshHost.Id,
|
|
NewtId = string.IsNullOrWhiteSpace(NewtId) ? null : NewtId.Trim(),
|
|
NewtSecret = string.IsNullOrWhiteSpace(NewtSecret) ? null : NewtSecret.Trim(),
|
|
CifsServer = string.IsNullOrWhiteSpace(CifsServer) ? null : CifsServer.Trim(),
|
|
CifsShareBasePath = string.IsNullOrWhiteSpace(CifsShareBasePath) ? null : CifsShareBasePath.Trim(),
|
|
CifsUsername = string.IsNullOrWhiteSpace(CifsUsername) ? null : CifsUsername.Trim(),
|
|
CifsPassword = string.IsNullOrWhiteSpace(CifsPassword) ? null : CifsPassword.Trim(),
|
|
CifsExtraOptions = string.IsNullOrWhiteSpace(CifsExtraOptions) ? null : CifsExtraOptions.Trim(),
|
|
};
|
|
|
|
var result = await instanceSvc.CreateInstanceAsync(dto);
|
|
|
|
AppendOutput(result.Output ?? string.Empty);
|
|
SetProgress(100, result.Success ? "Deployment complete!" : "Deployment failed.");
|
|
|
|
StatusMessage = result.Success
|
|
? $"Instance '{Abbrev}-cms-stack' deployed in {result.DurationMs}ms!"
|
|
: $"Deploy failed: {result.ErrorMessage}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Error: {ex.Message}";
|
|
AppendOutput(ex.ToString());
|
|
SetProgress(0, "Failed.");
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
private void SetProgress(double pct, string step)
|
|
{
|
|
ProgressPercent = pct;
|
|
ProgressStep = step;
|
|
AppendOutput($"[{pct:0}%] {step}");
|
|
}
|
|
|
|
private void AppendOutput(string text)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(text))
|
|
DeployOutput += (DeployOutput.Length > 0 ? "\n" : "") + text;
|
|
}
|
|
|
|
private static string GenerateRandomPassword(int length)
|
|
{
|
|
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
|
return RandomNumberGenerator.GetString(chars, length);
|
|
}
|
|
}
|
|
|