using System.Globalization; using System.Text; using Microsoft.Extensions.Logging; using OTSSignsOrchestrator.Core.Models.Entities; using OTSSignsOrchestrator.Core.Services; namespace OTSSignsOrchestrator.Desktop.Services; /// /// Docker Swarm secrets management over SSH. /// Uses docker CLI commands executed remotely instead of Docker.DotNet. /// public class SshDockerSecretsService : IDockerSecretsService { private readonly SshConnectionService _ssh; private readonly ILogger _logger; private SshHost? _currentHost; public SshDockerSecretsService(SshConnectionService ssh, ILogger logger) { _ssh = ssh; _logger = logger; } public void SetHost(SshHost host) => _currentHost = host; public SshHost? CurrentHost => _currentHost; public async Task<(bool Created, string SecretId)> EnsureSecretAsync(string name, string value, bool rotate = false) { EnsureHost(); _logger.LogInformation("Ensuring secret exists via SSH: {SecretName}", name); // Check if secret already exists var existing = await FindSecretAsync(name); if (existing != null && !rotate) { _logger.LogDebug("Secret already exists: {SecretName} (id={SecretId})", name, existing.Value.id); return (false, existing.Value.id); } if (existing != null && rotate) { _logger.LogInformation("Rotating secret via SSH: {SecretName} (old id={SecretId})", name, existing.Value.id); await _ssh.RunCommandAsync(_currentHost!, $"docker secret rm {name}"); } // Create secret via stdin var safeValue = value.Replace("'", "'\\''"); var (exitCode, stdout, stderr) = await _ssh.RunCommandAsync( _currentHost!, $"printf '%s' '{safeValue}' | docker secret create {name} -"); if (exitCode != 0) { _logger.LogError("Failed to create secret via SSH: {SecretName} | error={Error}", name, stderr); return (false, string.Empty); } var secretId = stdout.Trim(); _logger.LogInformation("Secret created via SSH: {SecretName} (id={SecretId})", name, secretId); return (true, secretId); } public async Task> ListSecretsAsync() { EnsureHost(); var (exitCode, stdout, _) = await _ssh.RunCommandAsync( _currentHost!, "docker secret ls --format '{{.ID}}\\t{{.Name}}\\t{{.CreatedAt}}'"); 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 SecretListItem { Id = parts.Length > 0 ? parts[0].Trim() : "", Name = parts.Length > 1 ? parts[1].Trim() : "", CreatedAt = parts.Length > 2 && DateTime.TryParse(parts[2].Trim(), CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue }; }) .ToList(); } public async Task DeleteSecretAsync(string name) { EnsureHost(); var existing = await FindSecretAsync(name); if (existing == null) { _logger.LogDebug("Secret not found for deletion: {SecretName}", name); return true; // idempotent } var (exitCode, _, stderr) = await _ssh.RunCommandAsync(_currentHost!, $"docker secret rm {name}"); if (exitCode != 0) { _logger.LogError("Failed to delete secret via SSH: {SecretName} | error={Error}", name, stderr); return false; } _logger.LogInformation("Secret deleted via SSH: {SecretName}", name); return true; } private async Task<(string id, string name)?> FindSecretAsync(string name) { var (exitCode, stdout, _) = await _ssh.RunCommandAsync( _currentHost!, $"docker secret ls --filter 'name={name}' --format '{{{{.ID}}}}\\t{{{{.Name}}}}'"); if (exitCode != 0 || string.IsNullOrWhiteSpace(stdout)) return null; var line = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries) .FirstOrDefault(l => { var parts = l.Split('\t', 2); return parts.Length > 1 && string.Equals(parts[1].Trim(), name, StringComparison.OrdinalIgnoreCase); }); if (line == null) return null; var p = line.Split('\t', 2); return (p[0].Trim(), p[1].Trim()); } private void EnsureHost() { if (_currentHost == null) throw new InvalidOperationException("No SSH host configured. Call SetHost() before using Docker secrets."); } }