Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
162 lines
6.9 KiB
C#
162 lines
6.9 KiB
C#
using System.Collections.ObjectModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using OTSSignsOrchestrator.Core.Data;
|
|
using OTSSignsOrchestrator.Core.Models.Entities;
|
|
using OTSSignsOrchestrator.Core.Services;
|
|
using OTSSignsOrchestrator.Desktop.Models;
|
|
using OTSSignsOrchestrator.Desktop.Services;
|
|
|
|
namespace OTSSignsOrchestrator.Desktop.ViewModels;
|
|
|
|
/// <summary>
|
|
/// ViewModel for listing, viewing, and managing CMS instances.
|
|
/// All data is fetched live from Docker Swarm hosts — nothing stored locally.
|
|
/// </summary>
|
|
public partial class InstancesViewModel : ObservableObject
|
|
{
|
|
private readonly IServiceProvider _services;
|
|
|
|
[ObservableProperty] private ObservableCollection<LiveStackItem> _instances = new();
|
|
[ObservableProperty] private LiveStackItem? _selectedInstance;
|
|
[ObservableProperty] private string _statusMessage = string.Empty;
|
|
[ObservableProperty] private bool _isBusy;
|
|
[ObservableProperty] private string _filterText = string.Empty;
|
|
[ObservableProperty] private ObservableCollection<ServiceInfo> _selectedServices = new();
|
|
|
|
// Available SSH hosts — loaded for display and used to scope operations
|
|
[ObservableProperty] private ObservableCollection<SshHost> _availableHosts = new();
|
|
[ObservableProperty] private SshHost? _selectedSshHost;
|
|
|
|
public InstancesViewModel(IServiceProvider services)
|
|
{
|
|
_services = services;
|
|
_ = RefreshAllAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates all SSH hosts, then calls docker stack ls on each to build the
|
|
/// live instance list. Only stacks matching *-cms-stack are shown.
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private async Task LoadInstancesAsync() => await RefreshAllAsync();
|
|
|
|
private async Task RefreshAllAsync()
|
|
{
|
|
IsBusy = true;
|
|
StatusMessage = "Loading live instances from all hosts...";
|
|
SelectedServices = new ObservableCollection<ServiceInfo>();
|
|
try
|
|
{
|
|
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);
|
|
|
|
var dockerCli = _services.GetRequiredService<SshDockerCliService>();
|
|
var all = new List<LiveStackItem>();
|
|
var errors = new List<string>();
|
|
|
|
foreach (var host in hosts)
|
|
{
|
|
try
|
|
{
|
|
dockerCli.SetHost(host);
|
|
var stacks = await dockerCli.ListStacksAsync();
|
|
foreach (var stack in stacks.Where(s => s.Name.EndsWith("-cms-stack")))
|
|
{
|
|
all.Add(new LiveStackItem
|
|
{
|
|
StackName = stack.Name,
|
|
CustomerAbbrev = stack.Name[..^10],
|
|
ServiceCount = stack.ServiceCount,
|
|
Host = host,
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex) { errors.Add($"{host.Label}: {ex.Message}"); }
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(FilterText))
|
|
all = all.Where(i =>
|
|
i.StackName.Contains(FilterText, StringComparison.OrdinalIgnoreCase) ||
|
|
i.CustomerAbbrev.Contains(FilterText, StringComparison.OrdinalIgnoreCase) ||
|
|
i.HostLabel.Contains(FilterText, StringComparison.OrdinalIgnoreCase)).ToList();
|
|
|
|
Instances = new ObservableCollection<LiveStackItem>(all);
|
|
var msg = $"Found {all.Count} instance(s) across {hosts.Count} host(s).";
|
|
if (errors.Count > 0) msg += $" | Errors: {string.Join(" | ", errors)}";
|
|
StatusMessage = msg;
|
|
}
|
|
catch (Exception ex) { StatusMessage = $"Error: {ex.Message}"; }
|
|
finally { IsBusy = false; }
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task InspectInstanceAsync()
|
|
{
|
|
if (SelectedInstance == null) return;
|
|
IsBusy = true;
|
|
StatusMessage = $"Inspecting '{SelectedInstance.StackName}'...";
|
|
try
|
|
{
|
|
var dockerCli = _services.GetRequiredService<SshDockerCliService>();
|
|
dockerCli.SetHost(SelectedInstance.Host);
|
|
var services = await dockerCli.InspectStackServicesAsync(SelectedInstance.StackName);
|
|
SelectedServices = new ObservableCollection<ServiceInfo>(services);
|
|
StatusMessage = $"Found {services.Count} service(s) in stack '{SelectedInstance.StackName}'.";
|
|
}
|
|
catch (Exception ex) { StatusMessage = $"Error inspecting: {ex.Message}"; }
|
|
finally { IsBusy = false; }
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task DeleteInstanceAsync()
|
|
{
|
|
if (SelectedInstance == null) return;
|
|
IsBusy = true;
|
|
StatusMessage = $"Deleting {SelectedInstance.StackName}...";
|
|
try
|
|
{
|
|
var dockerCli = _services.GetRequiredService<SshDockerCliService>();
|
|
dockerCli.SetHost(SelectedInstance.Host);
|
|
var dockerSecrets = _services.GetRequiredService<SshDockerSecretsService>();
|
|
dockerSecrets.SetHost(SelectedInstance.Host);
|
|
using var scope = _services.CreateScope();
|
|
var instanceSvc = scope.ServiceProvider.GetRequiredService<InstanceService>();
|
|
var result = await instanceSvc.DeleteInstanceAsync(
|
|
SelectedInstance.StackName, SelectedInstance.CustomerAbbrev);
|
|
StatusMessage = result.Success
|
|
? $"Instance '{SelectedInstance.StackName}' deleted."
|
|
: $"Delete failed: {result.ErrorMessage}";
|
|
await RefreshAllAsync();
|
|
}
|
|
catch (Exception ex) { StatusMessage = $"Error deleting: {ex.Message}"; }
|
|
finally { IsBusy = false; }
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task RotateMySqlPasswordAsync()
|
|
{
|
|
if (SelectedInstance == null) return;
|
|
IsBusy = true;
|
|
StatusMessage = $"Rotating MySQL password for {SelectedInstance.StackName}...";
|
|
try
|
|
{
|
|
var dockerCli = _services.GetRequiredService<SshDockerCliService>();
|
|
dockerCli.SetHost(SelectedInstance.Host);
|
|
var dockerSecrets = _services.GetRequiredService<SshDockerSecretsService>();
|
|
dockerSecrets.SetHost(SelectedInstance.Host);
|
|
using var scope = _services.CreateScope();
|
|
var instanceSvc = scope.ServiceProvider.GetRequiredService<InstanceService>();
|
|
var (ok, msg) = await instanceSvc.RotateMySqlPasswordAsync(SelectedInstance.StackName);
|
|
StatusMessage = ok ? $"Done: {msg}" : $"Failed: {msg}";
|
|
await RefreshAllAsync();
|
|
}
|
|
catch (Exception ex) { StatusMessage = $"Error rotating password: {ex.Message}"; }
|
|
finally { IsBusy = false; }
|
|
}
|
|
}
|