feat: Add main application views and structure
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
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.
This commit is contained in:
140
OTSSignsOrchestrator.Desktop/Services/SshDockerSecretsService.cs
Normal file
140
OTSSignsOrchestrator.Desktop/Services/SshDockerSecretsService.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OTSSignsOrchestrator.Core.Models.Entities;
|
||||
using OTSSignsOrchestrator.Core.Services;
|
||||
|
||||
namespace OTSSignsOrchestrator.Desktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Docker Swarm secrets management over SSH.
|
||||
/// Uses docker CLI commands executed remotely instead of Docker.DotNet.
|
||||
/// </summary>
|
||||
public class SshDockerSecretsService : IDockerSecretsService
|
||||
{
|
||||
private readonly SshConnectionService _ssh;
|
||||
private readonly ILogger<SshDockerSecretsService> _logger;
|
||||
private SshHost? _currentHost;
|
||||
|
||||
public SshDockerSecretsService(SshConnectionService ssh, ILogger<SshDockerSecretsService> 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<List<SecretListItem>> 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<SecretListItem>();
|
||||
|
||||
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<bool> 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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user