using System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OTSSignsOrchestrator.Core.Configuration; using OTSSignsOrchestrator.Core.Models.DTOs; using OTSSignsOrchestrator.Core.Models.Entities; using OTSSignsOrchestrator.Core.Services; namespace OTSSignsOrchestrator.Desktop.Services; /// /// Docker CLI service that executes docker commands on a remote host over SSH. /// Requires an SshHost to be set before use via SetHost(). /// public class SshDockerCliService : IDockerCliService { private readonly SshConnectionService _ssh; private readonly DockerOptions _options; private readonly ILogger _logger; private SshHost? _currentHost; public SshDockerCliService( SshConnectionService ssh, IOptions options, ILogger logger) { _ssh = ssh; _options = options.Value; _logger = logger; } /// /// Set the SSH host to use for Docker commands. /// public void SetHost(SshHost host) { _currentHost = host; } public SshHost? CurrentHost => _currentHost; public async Task DeployStackAsync(string stackName, string composeYaml, bool resolveImage = false) { EnsureHost(); var sw = Stopwatch.StartNew(); var args = "docker stack deploy --compose-file -"; if (resolveImage) args += " --resolve-image changed"; args += $" {stackName}"; _logger.LogInformation("Deploying stack via SSH: {StackName} on {Host}", stackName, _currentHost!.Label); var (exitCode, stdout, stderr) = await _ssh.RunCommandWithStdinAsync(_currentHost, args, composeYaml); sw.Stop(); var result = new DeploymentResultDto { StackName = stackName, Success = exitCode == 0, ExitCode = exitCode, Output = stdout, ErrorMessage = stderr, Message = exitCode == 0 ? "Success" : "Failed", DurationMs = sw.ElapsedMilliseconds }; if (result.Success) _logger.LogInformation("Stack deployed via SSH: {StackName} | duration={DurationMs}ms", stackName, result.DurationMs); else _logger.LogError("Stack deploy failed via SSH: {StackName} | error={Error}", stackName, result.ErrorMessage); return result; } public async Task RemoveStackAsync(string stackName) { EnsureHost(); var sw = Stopwatch.StartNew(); _logger.LogInformation("Removing stack via SSH: {StackName} on {Host}", stackName, _currentHost!.Label); var (exitCode, stdout, stderr) = await _ssh.RunCommandAsync(_currentHost, $"docker stack rm {stackName}"); sw.Stop(); var result = new DeploymentResultDto { StackName = stackName, Success = exitCode == 0, ExitCode = exitCode, Output = stdout, ErrorMessage = stderr, Message = exitCode == 0 ? "Success" : "Failed", DurationMs = sw.ElapsedMilliseconds }; if (result.Success) _logger.LogInformation("Stack removed via SSH: {StackName}", stackName); else _logger.LogError("Stack remove failed via SSH: {StackName} | error={Error}", stackName, result.ErrorMessage); return result; } public async Task> ListStacksAsync() { EnsureHost(); var (exitCode, stdout, _) = await _ssh.RunCommandAsync( _currentHost!, "docker stack ls --format '{{.Name}}\\t{{.Services}}'"); if (exitCode != 0 || string.IsNullOrWhiteSpace(stdout)) return new List(); return stdout .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(line => { var parts = line.Split('\t', 2); return new StackInfo { Name = parts[0].Trim(), ServiceCount = parts.Length > 1 && int.TryParse(parts[1].Trim(), out var c) ? c : 0 }; }) .ToList(); } public async Task> InspectStackServicesAsync(string stackName) { EnsureHost(); var (exitCode, stdout, _) = await _ssh.RunCommandAsync( _currentHost!, $"docker stack services {stackName} --format '{{{{.Name}}}}\\t{{{{.Image}}}}\\t{{{{.Replicas}}}}'"); if (exitCode != 0 || string.IsNullOrWhiteSpace(stdout)) return new List(); return stdout .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(line => { var parts = line.Split('\t', 3); return new ServiceInfo { Name = parts.Length > 0 ? parts[0].Trim() : "", Image = parts.Length > 1 ? parts[1].Trim() : "", Replicas = parts.Length > 2 ? parts[2].Trim() : "" }; }) .ToList(); } private void EnsureHost() { if (_currentHost == null) throw new InvalidOperationException("No SSH host configured. Call SetHost() before using Docker commands."); } }