refactor(configmanager): convert CLI to structured logging with Serilog
Replace Console.WriteLine calls with ILogger usage across all CLI commands. Serilog is configured via DI with clean message-only output suitable for CLI tooling. Log levels map to --quiet (Warning), default (Information), and --verbose (Debug) flags. - Add Serilog packages and configure in Program.cs - Convert all 7 command files to use ILoggerFactory from DI - Add BeginScope with context properties (Command, ConfigPath, etc.) - Create logging_style.md documenting patterns and best practices - Update tests with TestLoggingHelper for Serilog test configuration
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
@@ -104,38 +105,45 @@ public static class BackupCommands
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("BackupCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "backup create",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var backupPath = await backupService.CreateBackupAsync(appSettingsPath);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine("Backup created successfully");
|
||||
Console.WriteLine($"Path: {backupPath}");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var backupPath = await backupService.CreateBackupAsync(appSettingsPath);
|
||||
|
||||
logger.LogInformation("Backup created successfully");
|
||||
logger.LogInformation("Path: {BackupPath}", backupPath);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create backup");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,53 +153,56 @@ public static class BackupCommands
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("BackupCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
["Command"] = "backup list",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
var backups = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine($"=== Backups ({backups.Count}) ===");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (backups.Count == 0)
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine("No backups found");
|
||||
var backups = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
|
||||
logger.LogInformation("=== Backups ({Count}) ===", backups.Count);
|
||||
|
||||
if (backups.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No backups found");
|
||||
return 0;
|
||||
}
|
||||
|
||||
logger.LogInformation("{Timestamp,-20} {Size,-12} {Path}", "Timestamp", "Size", "Path");
|
||||
logger.LogInformation("{Separator}", new string('-', 60));
|
||||
|
||||
foreach (var backup in backups)
|
||||
{
|
||||
var timestamp = backup.Timestamp.ToString("yyyy-MM-dd_HHmmss");
|
||||
var size = FormatFileSize(backup.Size);
|
||||
var path = verbose ? backup.Path : Path.GetFileName(backup.Path);
|
||||
|
||||
logger.LogInformation("{Timestamp,-20} {Size,-12} {Path}", timestamp, size, path);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!quiet)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"{"Timestamp",-20} {"Size",-12} {"Path"}");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
logger.LogError(ex, "Failed to list backups");
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach (var backup in backups)
|
||||
{
|
||||
var timestamp = backup.Timestamp.ToString("yyyy-MM-dd_HHmmss");
|
||||
var size = FormatFileSize(backup.Size);
|
||||
var path = verbose ? backup.Path : Path.GetFileName(backup.Path);
|
||||
|
||||
Console.WriteLine($"{timestamp,-20} {size,-12} {path}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,62 +213,66 @@ public static class BackupCommands
|
||||
bool quiet,
|
||||
string timestamp)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("BackupCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
["Command"] = "backup restore",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["BackupTimestamp"] = timestamp
|
||||
}))
|
||||
{
|
||||
var backups = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var backup = backups.FirstOrDefault(b => b.Path.Contains(timestamp));
|
||||
|
||||
if (backup == null)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Backup with timestamp '{timestamp}' not found");
|
||||
|
||||
if (backups.Count > 0)
|
||||
{
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Available backups:");
|
||||
foreach (var b in backups.Take(5))
|
||||
{
|
||||
Console.Error.WriteLine($" {b.Timestamp:yyyy-MM-dd_HHmmss}");
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create a backup of current config before restoring
|
||||
if (File.Exists(appSettingsPath))
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
{
|
||||
var currentBackup = await backupService.CreateBackupAsync(appSettingsPath);
|
||||
if (!quiet)
|
||||
var backups = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var backup = backups.FirstOrDefault(b => b.Path.Contains(timestamp));
|
||||
|
||||
if (backup == null)
|
||||
{
|
||||
Console.WriteLine($"Current config backed up to: {Path.GetFileName(currentBackup)}");
|
||||
logger.LogError("Backup with timestamp '{Timestamp}' not found", timestamp);
|
||||
|
||||
if (backups.Count > 0)
|
||||
{
|
||||
logger.LogError("Available backups:");
|
||||
foreach (var b in backups.Take(5))
|
||||
{
|
||||
logger.LogError(" {Timestamp}", b.Timestamp.ToString("yyyy-MM-dd_HHmmss"));
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create a backup of current config before restoring
|
||||
if (File.Exists(appSettingsPath))
|
||||
{
|
||||
var currentBackup = await backupService.CreateBackupAsync(appSettingsPath);
|
||||
logger.LogInformation("Current config backed up to: {BackupName}", Path.GetFileName(currentBackup));
|
||||
}
|
||||
|
||||
await backupService.RestoreBackupAsync(backup.Path, appSettingsPath);
|
||||
|
||||
logger.LogInformation("Backup restored successfully");
|
||||
logger.LogInformation("Restored from: {BackupName}", Path.GetFileName(backup.Path));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
await backupService.RestoreBackupAsync(backup.Path, appSettingsPath);
|
||||
|
||||
if (!quiet)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Backup restored successfully");
|
||||
Console.WriteLine($"Restored from: {Path.GetFileName(backup.Path)}");
|
||||
logger.LogError(ex, "Failed to restore backup");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,52 +283,60 @@ public static class BackupCommands
|
||||
bool quiet,
|
||||
int keep)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("BackupCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (keep < 1)
|
||||
["Command"] = "backup cleanup",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["KeepCount"] = keep
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine("Error: --keep must be at least 1");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
{
|
||||
var backupsBefore = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var countBefore = backupsBefore.Count;
|
||||
|
||||
await backupService.CleanupOldBackupsAsync(appSettingsPath, keep);
|
||||
|
||||
var backupsAfter = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var deleted = countBefore - backupsAfter.Count;
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (keep < 1)
|
||||
{
|
||||
logger.LogError("--keep must be at least 1");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
{
|
||||
var backupsBefore = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var countBefore = backupsBefore.Count;
|
||||
|
||||
await backupService.CleanupOldBackupsAsync(appSettingsPath, keep);
|
||||
|
||||
var backupsAfter = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var deleted = countBefore - backupsAfter.Count;
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
Console.WriteLine($"Deleted {deleted} old backup(s)");
|
||||
Console.WriteLine($"Remaining: {backupsAfter.Count} backup(s)");
|
||||
logger.LogInformation("Deleted {Count} old backup(s)", deleted);
|
||||
logger.LogInformation("Remaining: {Count} backup(s)", backupsAfter.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("No backups to delete");
|
||||
Console.WriteLine($"Current count: {countBefore} (keeping {keep})");
|
||||
logger.LogInformation("No backups to delete");
|
||||
logger.LogInformation("Current count: {Count} (keeping {Keep})", countBefore, keep);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to cleanup backups");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
@@ -78,7 +79,7 @@ public static class ConfigCommands
|
||||
Option<bool> quietOption,
|
||||
Option<bool> jsonOption,
|
||||
Func<Core.Models.ConfigModel, T> selector,
|
||||
Action<T, bool> formatter)
|
||||
Action<T, bool, ILogger> formatter)
|
||||
{
|
||||
var command = new Command(name, description);
|
||||
|
||||
@@ -135,48 +136,55 @@ public static class ConfigCommands
|
||||
bool json,
|
||||
string sectionName,
|
||||
Func<Core.Models.ConfigModel, T> selector,
|
||||
Action<T, bool> formatter)
|
||||
Action<T, bool, ILogger> formatter)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConfigCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = $"config show {sectionName}",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var section = selector(config);
|
||||
|
||||
if (json)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(section, JsonOptions));
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
if (!quiet)
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var section = selector(config);
|
||||
|
||||
if (json)
|
||||
{
|
||||
Console.WriteLine($"=== {sectionName} ===");
|
||||
logger.LogInformation("{Data}", JsonSerializer.Serialize(section, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("=== {SectionName} ===", sectionName);
|
||||
formatter(section, verbose, logger);
|
||||
}
|
||||
formatter(section, verbose);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to show section");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,57 +195,64 @@ public static class ConfigCommands
|
||||
bool quiet,
|
||||
bool json)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConfigCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "config show connections",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
if (json)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
// Output only names and providers, no secrets
|
||||
var safeConnections = config.ConnectionStrings.Entries.Select(e => new
|
||||
{
|
||||
e.Name,
|
||||
Provider = e.Provider.ToString()
|
||||
});
|
||||
Console.WriteLine(JsonSerializer.Serialize(safeConnections, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
|
||||
Console.WriteLine($"{"Name",-25} {"Provider",-15}");
|
||||
Console.WriteLine(new string('-', 40));
|
||||
}
|
||||
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
{
|
||||
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15}");
|
||||
}
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
if (json)
|
||||
{
|
||||
// Output only names and providers, no secrets
|
||||
var safeConnections = config.ConnectionStrings.Entries.Select(e => new
|
||||
{
|
||||
e.Name,
|
||||
Provider = e.Provider.ToString()
|
||||
});
|
||||
logger.LogInformation("{Data}", JsonSerializer.Serialize(safeConnections, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("=== Connections ({Count}) ===", config.ConnectionStrings.Entries.Count);
|
||||
logger.LogInformation("{Name,-25} {Provider,-15}", "Name", "Provider");
|
||||
logger.LogInformation("{Separator}", new string('-', 40));
|
||||
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
{
|
||||
logger.LogInformation("{Name,-25} {Provider,-15}", entry.Name, entry.Provider);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to show connections");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,177 +263,187 @@ public static class ConfigCommands
|
||||
bool quiet,
|
||||
bool json)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConfigCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "config show all",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
if (json)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
// Sanitize passwords before outputting
|
||||
var sanitizedConfig = new
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
if (json)
|
||||
{
|
||||
config.DataSync,
|
||||
config.DataAccess,
|
||||
config.Auth,
|
||||
config.Ldap,
|
||||
config.Search,
|
||||
config.ExcelExport,
|
||||
config.SecureStore,
|
||||
config.Pipelines,
|
||||
Connections = config.ConnectionStrings.Entries.Select(e => new
|
||||
// Sanitize passwords before outputting
|
||||
var sanitizedConfig = new
|
||||
{
|
||||
e.Name,
|
||||
Provider = e.Provider.ToString()
|
||||
})
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(sanitizedConfig, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("=== DataSync ===");
|
||||
FormatDataSync(config.DataSync, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== DataAccess ===");
|
||||
FormatDataAccess(config.DataAccess, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== Auth ===");
|
||||
FormatAuth(config.Auth, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== LDAP ===");
|
||||
FormatLdap(config.Ldap, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== Search ===");
|
||||
FormatSearch(config.Search, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== ExcelExport ===");
|
||||
FormatExcelExport(config.ExcelExport, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== SecureStore ===");
|
||||
FormatSecureStore(config.SecureStore, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
|
||||
Console.WriteLine($"{"Name",-25} {"Provider",-15}");
|
||||
Console.WriteLine(new string('-', 40));
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
{
|
||||
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15}");
|
||||
config.DataSync,
|
||||
config.DataAccess,
|
||||
config.Auth,
|
||||
config.Ldap,
|
||||
config.Search,
|
||||
config.ExcelExport,
|
||||
config.SecureStore,
|
||||
config.Pipelines,
|
||||
Connections = config.ConnectionStrings.Entries.Select(e => new
|
||||
{
|
||||
e.Name,
|
||||
Provider = e.Provider.ToString()
|
||||
})
|
||||
};
|
||||
logger.LogInformation("{Data}", JsonSerializer.Serialize(sanitizedConfig, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("=== DataSync ===");
|
||||
FormatDataSync(config.DataSync, verbose, logger);
|
||||
logger.LogInformation("");
|
||||
|
||||
logger.LogInformation("=== DataAccess ===");
|
||||
FormatDataAccess(config.DataAccess, verbose, logger);
|
||||
logger.LogInformation("");
|
||||
|
||||
logger.LogInformation("=== Auth ===");
|
||||
FormatAuth(config.Auth, verbose, logger);
|
||||
logger.LogInformation("");
|
||||
|
||||
logger.LogInformation("=== LDAP ===");
|
||||
FormatLdap(config.Ldap, verbose, logger);
|
||||
logger.LogInformation("");
|
||||
|
||||
logger.LogInformation("=== Search ===");
|
||||
FormatSearch(config.Search, verbose, logger);
|
||||
logger.LogInformation("");
|
||||
|
||||
logger.LogInformation("=== ExcelExport ===");
|
||||
FormatExcelExport(config.ExcelExport, verbose, logger);
|
||||
logger.LogInformation("");
|
||||
|
||||
logger.LogInformation("=== SecureStore ===");
|
||||
FormatSecureStore(config.SecureStore, verbose, logger);
|
||||
logger.LogInformation("");
|
||||
|
||||
logger.LogInformation("=== Connections ({Count}) ===", config.ConnectionStrings.Entries.Count);
|
||||
logger.LogInformation("{Name,-25} {Provider,-15}", "Name", "Provider");
|
||||
logger.LogInformation("{Separator}", new string('-', 40));
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
{
|
||||
logger.LogInformation("{Name,-25} {Provider,-15}", entry.Name, entry.Provider);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to show all sections");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FormatDataSync(Core.Models.DataSyncSection section, bool verbose)
|
||||
private static void FormatDataSync(Core.Models.DataSyncSection section, bool verbose, ILogger logger)
|
||||
{
|
||||
Console.WriteLine($"Enabled: {section.Enabled}");
|
||||
Console.WriteLine($"CheckInterval: {section.CheckInterval}");
|
||||
Console.WriteLine($"MaxDegreeOfParallelism: {section.MaxDegreeOfParallelism}");
|
||||
Console.WriteLine($"BatchSize: {section.BatchSize}");
|
||||
Console.WriteLine($"BulkCopyBatchSize: {section.BulkCopyBatchSize}");
|
||||
logger.LogInformation("Enabled: {Value}", section.Enabled);
|
||||
logger.LogInformation("CheckInterval: {Value}", section.CheckInterval);
|
||||
logger.LogInformation("MaxDegreeOfParallelism: {Value}", section.MaxDegreeOfParallelism);
|
||||
logger.LogInformation("BatchSize: {Value}", section.BatchSize);
|
||||
logger.LogInformation("BulkCopyBatchSize: {Value}", section.BulkCopyBatchSize);
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"LookbackMultiplier: {section.LookbackMultiplier}");
|
||||
Console.WriteLine($"PurgeRetentionDays: {section.PurgeRetentionDays}");
|
||||
Console.WriteLine($"SyncTimeoutSeconds: {section.SyncTimeoutSeconds}");
|
||||
logger.LogDebug("LookbackMultiplier: {Value}", section.LookbackMultiplier);
|
||||
logger.LogDebug("PurgeRetentionDays: {Value}", section.PurgeRetentionDays);
|
||||
logger.LogDebug("SyncTimeoutSeconds: {Value}", section.SyncTimeoutSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
private static void FormatDataAccess(Core.Models.DataAccessSection section, bool verbose)
|
||||
private static void FormatDataAccess(Core.Models.DataAccessSection section, bool verbose, ILogger logger)
|
||||
{
|
||||
Console.WriteLine($"DefaultTimeoutSeconds: {section.DefaultTimeoutSeconds}");
|
||||
Console.WriteLine($"LotUsageTimeoutSeconds: {section.LotUsageTimeoutSeconds}");
|
||||
Console.WriteLine($"MisDataTimeoutSeconds: {section.MisDataTimeoutSeconds}");
|
||||
Console.WriteLine($"EnableDetailedLogging: {section.EnableDetailedLogging}");
|
||||
logger.LogInformation("DefaultTimeoutSeconds: {Value}", section.DefaultTimeoutSeconds);
|
||||
logger.LogInformation("LotUsageTimeoutSeconds: {Value}", section.LotUsageTimeoutSeconds);
|
||||
logger.LogInformation("MisDataTimeoutSeconds: {Value}", section.MisDataTimeoutSeconds);
|
||||
logger.LogInformation("EnableDetailedLogging: {Value}", section.EnableDetailedLogging);
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"ProductionSchema: {section.ProductionSchema}");
|
||||
Console.WriteLine($"ArchiveSchema: {section.ArchiveSchema}");
|
||||
Console.WriteLine($"StageSchema: {section.StageSchema}");
|
||||
logger.LogDebug("ProductionSchema: {Value}", section.ProductionSchema);
|
||||
logger.LogDebug("ArchiveSchema: {Value}", section.ArchiveSchema);
|
||||
logger.LogDebug("StageSchema: {Value}", section.StageSchema);
|
||||
}
|
||||
}
|
||||
|
||||
private static void FormatAuth(Core.Models.AuthSection section, bool verbose)
|
||||
private static void FormatAuth(Core.Models.AuthSection section, bool verbose, ILogger logger)
|
||||
{
|
||||
Console.WriteLine($"CookieName: {section.CookieName}");
|
||||
Console.WriteLine($"CookieExpirationMinutes: {section.CookieExpirationMinutes}");
|
||||
logger.LogInformation("CookieName: {Value}", section.CookieName);
|
||||
logger.LogInformation("CookieExpirationMinutes: {Value}", section.CookieExpirationMinutes);
|
||||
}
|
||||
|
||||
private static void FormatLdap(Core.Models.LdapSection section, bool verbose)
|
||||
private static void FormatLdap(Core.Models.LdapSection section, bool verbose, ILogger logger)
|
||||
{
|
||||
Console.WriteLine($"UseFakeAuth: {section.UseFakeAuth}");
|
||||
Console.WriteLine($"ConnectionTimeoutSeconds: {section.ConnectionTimeoutSeconds}");
|
||||
Console.WriteLine($"ServerUrls: {(section.ServerUrls.Length > 0 ? string.Join(", ", section.ServerUrls) : "(none)")}");
|
||||
Console.WriteLine($"GroupDn: {(string.IsNullOrEmpty(section.GroupDn) ? "(not set)" : section.GroupDn)}");
|
||||
logger.LogInformation("UseFakeAuth: {Value}", section.UseFakeAuth);
|
||||
logger.LogInformation("ConnectionTimeoutSeconds: {Value}", section.ConnectionTimeoutSeconds);
|
||||
logger.LogInformation("ServerUrls: {Value}", section.ServerUrls.Length > 0 ? string.Join(", ", section.ServerUrls) : "(none)");
|
||||
logger.LogInformation("GroupDn: {Value}", string.IsNullOrEmpty(section.GroupDn) ? "(not set)" : section.GroupDn);
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"SearchBase: {(string.IsNullOrEmpty(section.SearchBase) ? "(not set)" : section.SearchBase)}");
|
||||
Console.WriteLine($"AdminBypassUsers: {(section.AdminBypassUsers.Length > 0 ? string.Join(", ", section.AdminBypassUsers) : "(none)")}");
|
||||
logger.LogDebug("SearchBase: {Value}", string.IsNullOrEmpty(section.SearchBase) ? "(not set)" : section.SearchBase);
|
||||
logger.LogDebug("AdminBypassUsers: {Value}", section.AdminBypassUsers.Length > 0 ? string.Join(", ", section.AdminBypassUsers) : "(none)");
|
||||
}
|
||||
}
|
||||
|
||||
private static void FormatSearch(Core.Models.SearchSection section, bool verbose)
|
||||
private static void FormatSearch(Core.Models.SearchSection section, bool verbose, ILogger logger)
|
||||
{
|
||||
Console.WriteLine($"MaxResultRows: {section.MaxResultRows}");
|
||||
Console.WriteLine($"TimeoutSeconds: {section.TimeoutSeconds}");
|
||||
Console.WriteLine($"MaxConcurrentSearches: {section.MaxConcurrentSearches}");
|
||||
logger.LogInformation("MaxResultRows: {Value}", section.MaxResultRows);
|
||||
logger.LogInformation("TimeoutSeconds: {Value}", section.TimeoutSeconds);
|
||||
logger.LogInformation("MaxConcurrentSearches: {Value}", section.MaxConcurrentSearches);
|
||||
}
|
||||
|
||||
private static void FormatExcelExport(Core.Models.ExcelExportSection section, bool verbose)
|
||||
private static void FormatExcelExport(Core.Models.ExcelExportSection section, bool verbose, ILogger logger)
|
||||
{
|
||||
Console.WriteLine($"MaxRowsPerSheet: {section.MaxRowsPerSheet}");
|
||||
Console.WriteLine($"DefaultDateFormat: {section.DefaultDateFormat}");
|
||||
Console.WriteLine($"TimezoneId: {section.TimezoneId}");
|
||||
Console.WriteLine($"TimezoneAbbreviation: {section.TimezoneAbbreviation}");
|
||||
Console.WriteLine($"DebugWriteToFile: {section.DebugWriteToFile}");
|
||||
logger.LogInformation("MaxRowsPerSheet: {Value}", section.MaxRowsPerSheet);
|
||||
logger.LogInformation("DefaultDateFormat: {Value}", section.DefaultDateFormat);
|
||||
logger.LogInformation("TimezoneId: {Value}", section.TimezoneId);
|
||||
logger.LogInformation("TimezoneAbbreviation: {Value}", section.TimezoneAbbreviation);
|
||||
logger.LogInformation("DebugWriteToFile: {Value}", section.DebugWriteToFile);
|
||||
|
||||
if (verbose && section.DebugWriteToFile)
|
||||
{
|
||||
Console.WriteLine($"DebugOutputDirectory: {section.DebugOutputDirectory}");
|
||||
logger.LogDebug("DebugOutputDirectory: {Value}", section.DebugOutputDirectory);
|
||||
}
|
||||
|
||||
// Note: passwords are not shown
|
||||
Console.WriteLine("CriteriaSheetPassword: ********");
|
||||
Console.WriteLine("DataSheetPassword: ********");
|
||||
logger.LogInformation("CriteriaSheetPassword: {Value}", "********");
|
||||
logger.LogInformation("DataSheetPassword: {Value}", "********");
|
||||
}
|
||||
|
||||
private static void FormatSecureStore(Core.Models.SecureStoreSection section, bool verbose)
|
||||
private static void FormatSecureStore(Core.Models.SecureStoreSection section, bool verbose, ILogger logger)
|
||||
{
|
||||
Console.WriteLine($"StorePath: {section.StorePath}");
|
||||
Console.WriteLine($"KeyFilePath: {section.KeyFilePath}");
|
||||
Console.WriteLine($"AutoCreateStore: {section.AutoCreateStore}");
|
||||
Console.WriteLine($"RequiredKeys: {(section.RequiredKeys.Count > 0 ? string.Join(", ", section.RequiredKeys) : "(none)")}");
|
||||
logger.LogInformation("StorePath: {Value}", section.StorePath);
|
||||
logger.LogInformation("KeyFilePath: {Value}", section.KeyFilePath);
|
||||
logger.LogInformation("AutoCreateStore: {Value}", section.AutoCreateStore);
|
||||
logger.LogInformation("RequiredKeys: {Value}", section.RequiredKeys.Count > 0 ? string.Join(", ", section.RequiredKeys) : "(none)");
|
||||
}
|
||||
|
||||
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
|
||||
@@ -980,54 +1005,58 @@ public static class ConfigCommands
|
||||
string sectionName,
|
||||
Func<ConfigModel, bool> modifier)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConfigCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = $"config set {sectionName.ToLower()}",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var modified = modifier(config);
|
||||
|
||||
if (!modified)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: No changes specified. Use --help to see available options.");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create backup before saving
|
||||
var backupPath = await backupService.CreateBackupAsync(appSettingsPath);
|
||||
if (verbose)
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
Console.WriteLine($"Backup created: {Path.GetFileName(backupPath)}");
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
if (!quiet)
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"{sectionName} configuration updated successfully");
|
||||
}
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var modified = modifier(config);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
if (!modified)
|
||||
{
|
||||
logger.LogError("No changes specified. Use --help to see available options");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create backup before saving
|
||||
var backupPath = await backupService.CreateBackupAsync(appSettingsPath);
|
||||
logger.LogDebug("Backup created: {BackupName}", Path.GetFileName(backupPath));
|
||||
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
logger.LogInformation("{SectionName} configuration updated successfully", sectionName);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to update configuration");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
@@ -239,52 +240,59 @@ public static class ConnectionCommands
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConnectionCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "connection list",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
|
||||
Console.WriteLine($"{"Name",-25} {"Provider",-15} {"Server/Host",-30}");
|
||||
Console.WriteLine(new string('-', 70));
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
var serverOrHost = entry.Provider switch
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
logger.LogInformation("=== Connections ({Count}) ===", config.ConnectionStrings.Entries.Count);
|
||||
logger.LogInformation("{Name,-25} {Provider,-15} {ServerHost,-30}", "Name", "Provider", "Server/Host");
|
||||
logger.LogInformation("{Separator}", new string('-', 70));
|
||||
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
{
|
||||
ConnectionProvider.SqlServer => entry.Server ?? "-",
|
||||
ConnectionProvider.Oracle => entry.Host ?? "-",
|
||||
ConnectionProvider.Generic => "(raw)",
|
||||
_ => "-"
|
||||
};
|
||||
var serverOrHost = entry.Provider switch
|
||||
{
|
||||
ConnectionProvider.SqlServer => entry.Server ?? "-",
|
||||
ConnectionProvider.Oracle => entry.Host ?? "-",
|
||||
ConnectionProvider.Generic => "(raw)",
|
||||
_ => "-"
|
||||
};
|
||||
|
||||
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15} {serverOrHost,-30}");
|
||||
logger.LogInformation("{Name,-25} {Provider,-15} {ServerHost,-30}", entry.Name, entry.Provider, serverOrHost);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to list connections");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,84 +303,92 @@ public static class ConnectionCommands
|
||||
bool quiet,
|
||||
string name)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConnectionCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "connection show",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["ConnectionName"] = name
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (entry == null)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Connection '{name}' not found");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!quiet)
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
Console.WriteLine($"=== Connection: {entry.Name} ===");
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Name: {entry.Name}");
|
||||
Console.WriteLine($"Provider: {entry.Provider}");
|
||||
|
||||
switch (entry.Provider)
|
||||
try
|
||||
{
|
||||
case ConnectionProvider.SqlServer:
|
||||
Console.WriteLine($"Server: {entry.Server ?? "(not set)"}");
|
||||
if (entry.SqlServerPort.HasValue)
|
||||
Console.WriteLine($"Port: {entry.SqlServerPort.Value}");
|
||||
Console.WriteLine($"Database: {entry.Database ?? "(not set)"}");
|
||||
Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}");
|
||||
Console.WriteLine($"Password: {MaskPassword(entry.Password)}");
|
||||
Console.WriteLine($"Encrypt: {entry.Encrypt}");
|
||||
Console.WriteLine($"TrustServerCertificate: {entry.TrustServerCertificate}");
|
||||
Console.WriteLine($"ConnectionTimeout: {entry.ConnectionTimeout}");
|
||||
if (!string.IsNullOrEmpty(entry.ApplicationName))
|
||||
Console.WriteLine($"ApplicationName: {entry.ApplicationName}");
|
||||
break;
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
case ConnectionProvider.Oracle:
|
||||
Console.WriteLine($"Host: {entry.Host ?? "(not set)"}");
|
||||
Console.WriteLine($"Port: {entry.Port}");
|
||||
Console.WriteLine($"ServiceName: {entry.ServiceName ?? "(not set)"}");
|
||||
Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}");
|
||||
Console.WriteLine($"Password: {MaskPassword(entry.Password)}");
|
||||
break;
|
||||
if (entry == null)
|
||||
{
|
||||
logger.LogError("Connection '{Name}' not found", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
case ConnectionProvider.Generic:
|
||||
Console.WriteLine($"RawConnectionString: {MaskConnectionString(entry.RawConnectionString)}");
|
||||
break;
|
||||
logger.LogInformation("=== Connection: {Name} ===", entry.Name);
|
||||
|
||||
logger.LogInformation("Name: {Value}", entry.Name);
|
||||
logger.LogInformation("Provider: {Value}", entry.Provider);
|
||||
|
||||
switch (entry.Provider)
|
||||
{
|
||||
case ConnectionProvider.SqlServer:
|
||||
logger.LogInformation("Server: {Value}", entry.Server ?? "(not set)");
|
||||
if (entry.SqlServerPort.HasValue)
|
||||
logger.LogInformation("Port: {Value}", entry.SqlServerPort.Value);
|
||||
logger.LogInformation("Database: {Value}", entry.Database ?? "(not set)");
|
||||
logger.LogInformation("User ID: {Value}", entry.UserId ?? "(not set)");
|
||||
logger.LogInformation("Password: {Value}", MaskPassword(entry.Password));
|
||||
logger.LogInformation("Encrypt: {Value}", entry.Encrypt);
|
||||
logger.LogInformation("TrustServerCertificate: {Value}", entry.TrustServerCertificate);
|
||||
logger.LogInformation("ConnectionTimeout: {Value}", entry.ConnectionTimeout);
|
||||
if (!string.IsNullOrEmpty(entry.ApplicationName))
|
||||
logger.LogInformation("ApplicationName: {Value}", entry.ApplicationName);
|
||||
break;
|
||||
|
||||
case ConnectionProvider.Oracle:
|
||||
logger.LogInformation("Host: {Value}", entry.Host ?? "(not set)");
|
||||
logger.LogInformation("Port: {Value}", entry.Port);
|
||||
logger.LogInformation("ServiceName: {Value}", entry.ServiceName ?? "(not set)");
|
||||
logger.LogInformation("User ID: {Value}", entry.UserId ?? "(not set)");
|
||||
logger.LogInformation("Password: {Value}", MaskPassword(entry.Password));
|
||||
break;
|
||||
|
||||
case ConnectionProvider.Generic:
|
||||
logger.LogInformation("RawConnectionString: {Value}", MaskConnectionString(entry.RawConnectionString));
|
||||
break;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
logger.LogDebug("");
|
||||
logger.LogDebug("Generated connection string (masked):");
|
||||
logger.LogDebug(" {ConnectionString}", MaskConnectionString(entry.GenerateConnectionString()));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Generated connection string (masked):");
|
||||
Console.WriteLine($" {MaskConnectionString(entry.GenerateConnectionString())}");
|
||||
logger.LogError(ex, "Failed to show connection");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,76 +407,84 @@ public static class ConnectionCommands
|
||||
string? serviceName,
|
||||
string? raw)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConnectionCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "connection add",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["ConnectionName"] = name
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
// Check if connection already exists
|
||||
if (config.ConnectionStrings.Entries.Any(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Connection '{name}' already exists");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate required fields based on provider
|
||||
if (provider == ConnectionProvider.Generic && string.IsNullOrEmpty(raw))
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
Console.Error.WriteLine("Error: --raw is required for Generic provider");
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
var entry = new ConnectionStringEntry
|
||||
try
|
||||
{
|
||||
Name = name,
|
||||
Provider = provider,
|
||||
Server = server,
|
||||
Database = database,
|
||||
UserId = user,
|
||||
Password = password,
|
||||
Host = server, // For Oracle, use server as host
|
||||
ServiceName = serviceName,
|
||||
RawConnectionString = raw
|
||||
};
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
if (port.HasValue)
|
||||
{
|
||||
if (provider == ConnectionProvider.SqlServer)
|
||||
entry.SqlServerPort = port;
|
||||
else if (provider == ConnectionProvider.Oracle)
|
||||
entry.Port = port.Value;
|
||||
// Check if connection already exists
|
||||
if (config.ConnectionStrings.Entries.Any(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
logger.LogError("Connection '{Name}' already exists", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate required fields based on provider
|
||||
if (provider == ConnectionProvider.Generic && string.IsNullOrEmpty(raw))
|
||||
{
|
||||
logger.LogError("--raw is required for Generic provider");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Name = name,
|
||||
Provider = provider,
|
||||
Server = server,
|
||||
Database = database,
|
||||
UserId = user,
|
||||
Password = password,
|
||||
Host = server, // For Oracle, use server as host
|
||||
ServiceName = serviceName,
|
||||
RawConnectionString = raw
|
||||
};
|
||||
|
||||
if (port.HasValue)
|
||||
{
|
||||
if (provider == ConnectionProvider.SqlServer)
|
||||
entry.SqlServerPort = port;
|
||||
else if (provider == ConnectionProvider.Oracle)
|
||||
entry.Port = port.Value;
|
||||
}
|
||||
|
||||
config.ConnectionStrings.Entries.Add(entry);
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
logger.LogInformation("Connection '{Name}' added successfully", name);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
config.ConnectionStrings.Entries.Add(entry);
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
if (!quiet)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Connection '{name}' added successfully");
|
||||
logger.LogError(ex, "Failed to add connection");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,48 +495,56 @@ public static class ConnectionCommands
|
||||
bool quiet,
|
||||
string name)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConnectionCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "connection remove",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["ConnectionName"] = name
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (entry == null)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Connection '{name}' not found");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
config.ConnectionStrings.Entries.Remove(entry);
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!quiet)
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
Console.WriteLine($"Connection '{name}' removed successfully");
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
logger.LogError("Connection '{Name}' not found", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
config.ConnectionStrings.Entries.Remove(entry);
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
logger.LogInformation("Connection '{Name}' removed successfully", name);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to remove connection");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,100 +563,108 @@ public static class ConnectionCommands
|
||||
string? serviceName,
|
||||
string? raw)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ConnectionCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "connection update",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["ConnectionName"] = name
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (entry == null)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Connection '{name}' not found");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var modified = false;
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
if (provider.HasValue)
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
entry.Provider = provider.Value;
|
||||
modified = true;
|
||||
}
|
||||
if (server != null)
|
||||
{
|
||||
entry.Server = server;
|
||||
entry.Host = server; // For Oracle, use server as host
|
||||
modified = true;
|
||||
}
|
||||
if (database != null)
|
||||
{
|
||||
entry.Database = database;
|
||||
modified = true;
|
||||
}
|
||||
if (user != null)
|
||||
{
|
||||
entry.UserId = user;
|
||||
modified = true;
|
||||
}
|
||||
if (password != null)
|
||||
{
|
||||
entry.Password = password;
|
||||
modified = true;
|
||||
}
|
||||
if (port.HasValue)
|
||||
{
|
||||
if (entry.Provider == ConnectionProvider.SqlServer)
|
||||
entry.SqlServerPort = port;
|
||||
else if (entry.Provider == ConnectionProvider.Oracle)
|
||||
entry.Port = port.Value;
|
||||
modified = true;
|
||||
}
|
||||
if (serviceName != null)
|
||||
{
|
||||
entry.ServiceName = serviceName;
|
||||
modified = true;
|
||||
}
|
||||
if (raw != null)
|
||||
{
|
||||
entry.RawConnectionString = raw;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
Console.Error.WriteLine("Error: No changes specified. Use --help to see available options.");
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
if (!quiet)
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"Connection '{name}' updated successfully");
|
||||
}
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
if (entry == null)
|
||||
{
|
||||
logger.LogError("Connection '{Name}' not found", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
var modified = false;
|
||||
|
||||
if (provider.HasValue)
|
||||
{
|
||||
entry.Provider = provider.Value;
|
||||
modified = true;
|
||||
}
|
||||
if (server != null)
|
||||
{
|
||||
entry.Server = server;
|
||||
entry.Host = server; // For Oracle, use server as host
|
||||
modified = true;
|
||||
}
|
||||
if (database != null)
|
||||
{
|
||||
entry.Database = database;
|
||||
modified = true;
|
||||
}
|
||||
if (user != null)
|
||||
{
|
||||
entry.UserId = user;
|
||||
modified = true;
|
||||
}
|
||||
if (password != null)
|
||||
{
|
||||
entry.Password = password;
|
||||
modified = true;
|
||||
}
|
||||
if (port.HasValue)
|
||||
{
|
||||
if (entry.Provider == ConnectionProvider.SqlServer)
|
||||
entry.SqlServerPort = port;
|
||||
else if (entry.Provider == ConnectionProvider.Oracle)
|
||||
entry.Port = port.Value;
|
||||
modified = true;
|
||||
}
|
||||
if (serviceName != null)
|
||||
{
|
||||
entry.ServiceName = serviceName;
|
||||
modified = true;
|
||||
}
|
||||
if (raw != null)
|
||||
{
|
||||
entry.RawConnectionString = raw;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
logger.LogError("No changes specified. Use --help to see available options");
|
||||
return 1;
|
||||
}
|
||||
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
logger.LogInformation("Connection '{Name}' updated successfully", name);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to update connection");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
@@ -167,44 +168,51 @@ public static class PipelineCommands
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("PipelineCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
|
||||
|
||||
try
|
||||
["Command"] = "pipeline list",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine($"=== Pipelines ({pipelines.Count}) ===");
|
||||
Console.WriteLine($"{"Name",-20} {"Enabled",-8} {"Mass",-8} {"Daily",-8} {"Hourly",-8}");
|
||||
Console.WriteLine(new string('-', 52));
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach (var kvp in pipelines.OrderBy(p => p.Key))
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
|
||||
|
||||
try
|
||||
{
|
||||
var p = kvp.Value;
|
||||
var mass = FormatInterval(p.MassSyncIntervalMinutes);
|
||||
var daily = FormatInterval(p.DailySyncIntervalMinutes);
|
||||
var hourly = FormatInterval(p.HourlySyncIntervalMinutes);
|
||||
var enabled = p.IsEnabled ? "Yes" : "No";
|
||||
var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
|
||||
|
||||
Console.WriteLine($"{p.Name,-20} {enabled,-8} {mass,-8} {daily,-8} {hourly,-8}");
|
||||
logger.LogInformation("=== Pipelines ({Count}) ===", pipelines.Count);
|
||||
logger.LogInformation("{Name,-20} {Enabled,-8} {Mass,-8} {Daily,-8} {Hourly,-8}", "Name", "Enabled", "Mass", "Daily", "Hourly");
|
||||
logger.LogInformation("{Separator}", new string('-', 52));
|
||||
|
||||
foreach (var kvp in pipelines.OrderBy(p => p.Key))
|
||||
{
|
||||
var p = kvp.Value;
|
||||
var mass = FormatInterval(p.MassSyncIntervalMinutes);
|
||||
var daily = FormatInterval(p.DailySyncIntervalMinutes);
|
||||
var hourly = FormatInterval(p.HourlySyncIntervalMinutes);
|
||||
var enabled = p.IsEnabled ? "Yes" : "No";
|
||||
|
||||
logger.LogInformation("{Name,-20} {Enabled,-8} {Mass,-8} {Daily,-8} {Hourly,-8}", p.Name, enabled, mass, daily, hourly);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to list pipelines");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,86 +223,94 @@ public static class PipelineCommands
|
||||
bool quiet,
|
||||
string name)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("PipelineCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
if (!File.Exists(pipelinePath))
|
||||
["Command"] = "pipeline show",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["PipelineName"] = name
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine($"=== Pipeline: {pipeline.Name} ===");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Name: {pipeline.Name}");
|
||||
Console.WriteLine($"Enabled: {pipeline.IsEnabled}");
|
||||
Console.WriteLine($"Manual Only: {pipeline.IsManualOnly}");
|
||||
Console.WriteLine();
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
Console.WriteLine("Sync Intervals:");
|
||||
Console.WriteLine($" Mass: {FormatInterval(pipeline.MassSyncIntervalMinutes)}");
|
||||
Console.WriteLine($" Daily: {FormatInterval(pipeline.DailySyncIntervalMinutes)}");
|
||||
Console.WriteLine($" Hourly: {FormatInterval(pipeline.HourlySyncIntervalMinutes)}");
|
||||
|
||||
if (pipeline.Source != null)
|
||||
if (!File.Exists(pipelinePath))
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Source:");
|
||||
Console.WriteLine($" Connection: {pipeline.Source.Connection}");
|
||||
if (!string.IsNullOrEmpty(pipeline.Source.Query))
|
||||
Console.WriteLine($" Query: {(pipeline.Source.Query.Length > 50 ? pipeline.Source.Query[..50] + "..." : pipeline.Source.Query)}");
|
||||
logger.LogError("Pipeline '{Name}' not found", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (pipeline.Destination != null)
|
||||
try
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Destination:");
|
||||
Console.WriteLine($" Table: {pipeline.Destination.Table}");
|
||||
if (pipeline.Destination.MatchColumns.Count > 0)
|
||||
Console.WriteLine($" Match Columns: {string.Join(", ", pipeline.Destination.MatchColumns)}");
|
||||
}
|
||||
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
if (pipeline.PreScripts.Count > 0)
|
||||
logger.LogInformation("=== Pipeline: {Name} ===", pipeline.Name);
|
||||
|
||||
logger.LogInformation("Name: {Value}", pipeline.Name);
|
||||
logger.LogInformation("Enabled: {Value}", pipeline.IsEnabled);
|
||||
logger.LogInformation("Manual Only: {Value}", pipeline.IsManualOnly);
|
||||
logger.LogInformation("");
|
||||
|
||||
logger.LogInformation("Sync Intervals:");
|
||||
logger.LogInformation(" Mass: {Value}", FormatInterval(pipeline.MassSyncIntervalMinutes));
|
||||
logger.LogInformation(" Daily: {Value}", FormatInterval(pipeline.DailySyncIntervalMinutes));
|
||||
logger.LogInformation(" Hourly: {Value}", FormatInterval(pipeline.HourlySyncIntervalMinutes));
|
||||
|
||||
if (pipeline.Source != null)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Pre-Scripts: {pipeline.PreScripts.Count}");
|
||||
logger.LogInformation("");
|
||||
logger.LogInformation("Source:");
|
||||
logger.LogInformation(" Connection: {Value}", pipeline.Source.Connection);
|
||||
if (!string.IsNullOrEmpty(pipeline.Source.Query))
|
||||
logger.LogInformation(" Query: {Value}", pipeline.Source.Query.Length > 50 ? pipeline.Source.Query[..50] + "..." : pipeline.Source.Query);
|
||||
}
|
||||
|
||||
if (pipeline.Transforms.Count > 0)
|
||||
if (pipeline.Destination != null)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Transforms: {pipeline.Transforms.Count}");
|
||||
logger.LogInformation("");
|
||||
logger.LogInformation("Destination:");
|
||||
logger.LogInformation(" Table: {Value}", pipeline.Destination.Table);
|
||||
if (pipeline.Destination.MatchColumns.Count > 0)
|
||||
logger.LogInformation(" Match Columns: {Value}", string.Join(", ", pipeline.Destination.MatchColumns));
|
||||
}
|
||||
|
||||
if (pipeline.PostScripts.Count > 0)
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Post-Scripts: {pipeline.PostScripts.Count}");
|
||||
if (pipeline.PreScripts.Count > 0)
|
||||
{
|
||||
logger.LogDebug("");
|
||||
logger.LogDebug("Pre-Scripts: {Count}", pipeline.PreScripts.Count);
|
||||
}
|
||||
|
||||
if (pipeline.Transforms.Count > 0)
|
||||
{
|
||||
logger.LogDebug("");
|
||||
logger.LogDebug("Transforms: {Count}", pipeline.Transforms.Count);
|
||||
}
|
||||
|
||||
if (pipeline.PostScripts.Count > 0)
|
||||
{
|
||||
logger.LogDebug("");
|
||||
logger.LogDebug("Post-Scripts: {Count}", pipeline.PostScripts.Count);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to show pipeline");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,54 +323,62 @@ public static class PipelineCommands
|
||||
bool enabled,
|
||||
bool manualOnly)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("PipelineCommands");
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
|
||||
var pipelinePath = Path.Combine(pipelinesDir, $"pipeline.{name}.json");
|
||||
|
||||
if (File.Exists(pipelinePath))
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Pipeline '{name}' already exists");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
["Command"] = "pipeline create",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["PipelineName"] = name
|
||||
}))
|
||||
{
|
||||
// Create Pipelines directory if it doesn't exist
|
||||
if (!Directory.Exists(pipelinesDir))
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Directory.CreateDirectory(pipelinesDir);
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Name = name,
|
||||
IsEnabled = enabled,
|
||||
IsManualOnly = manualOnly,
|
||||
Source = new SourceElement { Connection = "jde" },
|
||||
Destination = new DestinationElement { Table = name }
|
||||
};
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
|
||||
var pipelinePath = Path.Combine(pipelinesDir, $"pipeline.{name}.json");
|
||||
|
||||
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
|
||||
|
||||
if (!quiet)
|
||||
if (File.Exists(pipelinePath))
|
||||
{
|
||||
Console.WriteLine($"Pipeline '{name}' created successfully");
|
||||
Console.WriteLine($"File: {pipelinePath}");
|
||||
logger.LogError("Pipeline '{Name}' already exists", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
try
|
||||
{
|
||||
// Create Pipelines directory if it doesn't exist
|
||||
if (!Directory.Exists(pipelinesDir))
|
||||
{
|
||||
Directory.CreateDirectory(pipelinesDir);
|
||||
}
|
||||
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Name = name,
|
||||
IsEnabled = enabled,
|
||||
IsManualOnly = manualOnly,
|
||||
Source = new SourceElement { Connection = "jde" },
|
||||
Destination = new DestinationElement { Table = name }
|
||||
};
|
||||
|
||||
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
|
||||
|
||||
logger.LogInformation("Pipeline '{Name}' created successfully", name);
|
||||
logger.LogInformation("File: {Path}", pipelinePath);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create pipeline");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,50 +390,57 @@ public static class PipelineCommands
|
||||
string name,
|
||||
bool force)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("PipelineCommands");
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
if (!File.Exists(pipelinePath))
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!force)
|
||||
["Command"] = "pipeline delete",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["PipelineName"] = name
|
||||
}))
|
||||
{
|
||||
Console.Write($"Delete pipeline '{name}'? [y/N] ");
|
||||
var response = Console.ReadLine();
|
||||
if (!string.Equals(response, "y", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(response, "yes", StringComparison.OrdinalIgnoreCase))
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine("Cancelled");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
if (!File.Exists(pipelinePath))
|
||||
{
|
||||
logger.LogError("Pipeline '{Name}' not found", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!force)
|
||||
{
|
||||
Console.Write($"Delete pipeline '{name}'? [y/N] ");
|
||||
var response = Console.ReadLine();
|
||||
if (!string.Equals(response, "y", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(response, "yes", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogInformation("Cancelled");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await configFileService.DeletePipelineFileAsync(pipelinePath);
|
||||
|
||||
logger.LogInformation("Pipeline '{Name}' deleted successfully", name);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await configFileService.DeletePipelineFileAsync(pipelinePath);
|
||||
|
||||
if (!quiet)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Pipeline '{name}' deleted successfully");
|
||||
logger.LogError(ex, "Failed to delete pipeline");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,40 +452,49 @@ public static class PipelineCommands
|
||||
string name,
|
||||
bool enabled)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("PipelineCommands");
|
||||
|
||||
var action = enabled ? "enable" : "disable";
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
if (!File.Exists(pipelinePath))
|
||||
["Command"] = $"pipeline {action}",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["PipelineName"] = name
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
|
||||
pipeline.IsEnabled = enabled;
|
||||
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
var action = enabled ? "enabled" : "disabled";
|
||||
Console.WriteLine($"Pipeline '{name}' {action} successfully");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
if (!File.Exists(pipelinePath))
|
||||
{
|
||||
logger.LogError("Pipeline '{Name}' not found", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
|
||||
pipeline.IsEnabled = enabled;
|
||||
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
|
||||
|
||||
var actionPast = enabled ? "enabled" : "disabled";
|
||||
logger.LogInformation("Pipeline '{Name}' {Action} successfully", name, actionPast);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to {Action} pipeline", action);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.ConfigManager.Core.Services.SecureStore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
@@ -138,29 +139,36 @@ public static class SecretCommands
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath);
|
||||
if (manager == null)
|
||||
return 1;
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("SecretCommands");
|
||||
|
||||
try
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
var keys = manager.GetKeys();
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== SecureStore Keys ({keys.Count}) ===");
|
||||
}
|
||||
|
||||
foreach (var key in keys.OrderBy(k => k))
|
||||
{
|
||||
Console.WriteLine(key);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
finally
|
||||
["Command"] = "secret list",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
manager.CloseStore();
|
||||
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger);
|
||||
if (manager == null)
|
||||
return 1;
|
||||
|
||||
try
|
||||
{
|
||||
var keys = manager.GetKeys();
|
||||
|
||||
logger.LogInformation("=== SecureStore Keys ({Count}) ===", keys.Count);
|
||||
|
||||
foreach (var key in keys.OrderBy(k => k))
|
||||
{
|
||||
logger.LogInformation("{Key}", key);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,24 +179,35 @@ public static class SecretCommands
|
||||
bool quiet,
|
||||
string key)
|
||||
{
|
||||
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath);
|
||||
if (manager == null)
|
||||
return 1;
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("SecretCommands");
|
||||
|
||||
try
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
var value = manager.GetSecret(key);
|
||||
Console.WriteLine(value);
|
||||
return 0;
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
["Command"] = "secret get",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["SecretKey"] = key
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Secret '{key}' not found");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger);
|
||||
if (manager == null)
|
||||
return 1;
|
||||
|
||||
try
|
||||
{
|
||||
var value = manager.GetSecret(key);
|
||||
logger.LogInformation("{Value}", value);
|
||||
return 0;
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
logger.LogError("Secret '{Key}' not found", key);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,30 +219,38 @@ public static class SecretCommands
|
||||
string key,
|
||||
string value)
|
||||
{
|
||||
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath);
|
||||
if (manager == null)
|
||||
return 1;
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("SecretCommands");
|
||||
|
||||
try
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
manager.SetSecret(key, value);
|
||||
manager.Save();
|
||||
["Command"] = "secret set",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["SecretKey"] = key
|
||||
}))
|
||||
{
|
||||
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger);
|
||||
if (manager == null)
|
||||
return 1;
|
||||
|
||||
if (!quiet)
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"Secret '{key}' set successfully");
|
||||
}
|
||||
manager.SetSecret(key, value);
|
||||
manager.Save();
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
logger.LogInformation("Secret '{Key}' set successfully", key);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to set secret");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,35 +261,43 @@ public static class SecretCommands
|
||||
bool quiet,
|
||||
string key)
|
||||
{
|
||||
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath);
|
||||
if (manager == null)
|
||||
return 1;
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("SecretCommands");
|
||||
|
||||
try
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
manager.RemoveSecret(key);
|
||||
manager.Save();
|
||||
["Command"] = "secret remove",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["SecretKey"] = key
|
||||
}))
|
||||
{
|
||||
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger);
|
||||
if (manager == null)
|
||||
return 1;
|
||||
|
||||
if (!quiet)
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"Secret '{key}' removed successfully");
|
||||
}
|
||||
manager.RemoveSecret(key);
|
||||
manager.Save();
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Secret '{key}' not found");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
logger.LogInformation("Secret '{Key}' removed successfully", key);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
logger.LogError("Secret '{Key}' not found", key);
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to remove secret");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,56 +309,64 @@ public static class SecretCommands
|
||||
string? storePath,
|
||||
string? keyPath)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("SecretCommands");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var effectiveStorePath = storePath ?? Path.Combine(folderPath, "data", "secrets.json");
|
||||
var effectiveKeyPath = keyPath ?? Path.Combine(folderPath, "data", "secrets.key");
|
||||
|
||||
if (File.Exists(effectiveStorePath))
|
||||
["Command"] = "secret init",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Store already exists at {effectiveStorePath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var manager = serviceProvider.GetRequiredService<ISecureStoreManager>();
|
||||
|
||||
try
|
||||
{
|
||||
manager.CreateStore(effectiveStorePath, effectiveKeyPath);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine("SecureStore initialized successfully");
|
||||
Console.WriteLine($"Store: {effectiveStorePath}");
|
||||
Console.WriteLine($"Key: {effectiveKeyPath}");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error creating store: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
var effectiveStorePath = storePath ?? Path.Combine(folderPath, "data", "secrets.json");
|
||||
var effectiveKeyPath = keyPath ?? Path.Combine(folderPath, "data", "secrets.key");
|
||||
|
||||
if (File.Exists(effectiveStorePath))
|
||||
{
|
||||
logger.LogError("Store already exists at {StorePath}", effectiveStorePath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
var manager = serviceProvider.GetRequiredService<ISecureStoreManager>();
|
||||
|
||||
try
|
||||
{
|
||||
manager.CreateStore(effectiveStorePath, effectiveKeyPath);
|
||||
|
||||
logger.LogInformation("SecureStore initialized successfully");
|
||||
logger.LogInformation("Store: {StorePath}", effectiveStorePath);
|
||||
logger.LogInformation("Key: {KeyPath}", effectiveKeyPath);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create store");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager.CloseStore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(ISecureStoreManager?, string?)> OpenStoreAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath)
|
||||
string? configPath,
|
||||
ILogger logger)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
@@ -333,7 +376,7 @@ public static class SecretCommands
|
||||
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
@@ -351,14 +394,14 @@ public static class SecretCommands
|
||||
|
||||
if (!File.Exists(storePath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: SecureStore not found at {storePath}");
|
||||
Console.Error.WriteLine("Use 'secret init' to create a new store.");
|
||||
logger.LogError("SecureStore not found at {Path}", storePath);
|
||||
logger.LogError("Use 'secret init' to create a new store");
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
if (!File.Exists(keyPath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Key file not found at {keyPath}");
|
||||
logger.LogError("Key file not found at {Path}", keyPath);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
@@ -369,7 +412,7 @@ public static class SecretCommands
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error opening store: {ex.Message}");
|
||||
logger.LogError(ex, "Failed to open store");
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
@@ -90,68 +91,73 @@ public static class TestConnectionCommand
|
||||
string? connectionName,
|
||||
ConnectionProvider provider)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("TestConnectionCommand");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
|
||||
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = $"test-connection {provider.ToString().ToLower()}",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["ConnectionName"] = connectionName ?? "(auto)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
var entry = connectionName != null
|
||||
? config.ConnectionStrings.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(connectionName, StringComparison.OrdinalIgnoreCase))
|
||||
: config.ConnectionStrings.Entries.FirstOrDefault(e => e.Provider == provider);
|
||||
|
||||
if (entry == null)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
var message = connectionName != null
|
||||
? $"Connection '{connectionName}' not found"
|
||||
: $"No {provider} connection found";
|
||||
Console.Error.WriteLine($"Error: {message}");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!quiet)
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
|
||||
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
Console.WriteLine($"Testing connection: {entry.Name}");
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
var connectionString = entry.GenerateConnectionString();
|
||||
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
|
||||
|
||||
if (result.Success)
|
||||
try
|
||||
{
|
||||
if (!quiet)
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
var entry = connectionName != null
|
||||
? config.ConnectionStrings.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(connectionName, StringComparison.OrdinalIgnoreCase))
|
||||
: config.ConnectionStrings.Entries.FirstOrDefault(e => e.Provider == provider);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
Console.WriteLine($"Status: Success");
|
||||
if (result.Duration.HasValue)
|
||||
Console.WriteLine($"Duration: {result.Duration.Value.TotalMilliseconds:F0}ms");
|
||||
var message = connectionName != null
|
||||
? $"Connection '{connectionName}' not found"
|
||||
: $"No {provider} connection found";
|
||||
logger.LogError("{Message}", message);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"Status: Failed");
|
||||
Console.Error.WriteLine($"Message: {result.Message}");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
logger.LogInformation("Testing connection: {Name}", entry.Name);
|
||||
|
||||
var connectionString = entry.GenerateConnectionString();
|
||||
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
logger.LogInformation("Status: Success");
|
||||
if (result.Duration.HasValue)
|
||||
logger.LogInformation("Duration: {Duration}ms", result.Duration.Value.TotalMilliseconds.ToString("F0"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
logger.LogError("Status: Failed");
|
||||
logger.LogError("Message: {Message}", result.Message);
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to test connection");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,70 +167,73 @@ public static class TestConnectionCommand
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("TestConnectionCommand");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
|
||||
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "test-connection all",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var hasFailures = false;
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine("=== Testing All Connections ===");
|
||||
Console.WriteLine($"Found {config.ConnectionStrings.Entries.Count} connection(s)");
|
||||
Console.WriteLine();
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach (var entry in config.ConnectionStrings.Entries)
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
|
||||
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
if (entry.Provider == ConnectionProvider.Generic)
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine($"{entry.Name}: Skipped (generic provider)");
|
||||
continue;
|
||||
}
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
var connectionString = entry.GenerateConnectionString();
|
||||
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var hasFailures = false;
|
||||
|
||||
if (result.Success)
|
||||
logger.LogInformation("=== Testing All Connections ===");
|
||||
logger.LogInformation("Found {Count} connection(s)", config.ConnectionStrings.Entries.Count);
|
||||
logger.LogInformation("");
|
||||
|
||||
foreach (var entry in config.ConnectionStrings.Entries)
|
||||
{
|
||||
if (!quiet)
|
||||
if (entry.Provider == ConnectionProvider.Generic)
|
||||
{
|
||||
logger.LogInformation("{Name}: Skipped (generic provider)", entry.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
var connectionString = entry.GenerateConnectionString();
|
||||
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
var duration = result.Duration.HasValue
|
||||
? $" ({result.Duration.Value.TotalMilliseconds:F0}ms)"
|
||||
: "";
|
||||
Console.WriteLine($"{entry.Name}: OK{duration}");
|
||||
logger.LogInformation("{Name}: OK{Duration}", entry.Name, duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
hasFailures = true;
|
||||
logger.LogError("{Name}: FAILED - {Message}", entry.Name, result.Message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hasFailures = true;
|
||||
Console.Error.WriteLine($"{entry.Name}: FAILED - {result.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return hasFailures ? 1 : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
return hasFailures ? 1 : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to test connections");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
@@ -96,58 +97,63 @@ public static class ValidateCommand
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ValidateCommand");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var validationService = serviceProvider.GetRequiredService<IValidationService>();
|
||||
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
if (!File.Exists(appSettingsPath))
|
||||
["Command"] = "validate appsettings",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var result = validationService.ValidateAppSettings(config);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine("=== AppSettings Validation ===");
|
||||
Console.WriteLine($"File: {appSettingsPath}");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (result.IsValid)
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var validationService = serviceProvider.GetRequiredService<IValidationService>();
|
||||
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
if (!File.Exists(appSettingsPath))
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine("Status: Valid");
|
||||
return 0;
|
||||
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {error}");
|
||||
}
|
||||
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
var result = validationService.ValidateAppSettings(config);
|
||||
|
||||
foreach (var warning in result.Warnings)
|
||||
logger.LogInformation("=== AppSettings Validation ===");
|
||||
logger.LogInformation("File: {Path}", appSettingsPath);
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
logger.LogInformation("Status: Valid");
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
logger.LogError("{Error}", error);
|
||||
}
|
||||
|
||||
foreach (var warning in result.Warnings)
|
||||
{
|
||||
logger.LogWarning("{Warning}", warning);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
catch (ConfigLoadException ex)
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine($"Warning: {warning}");
|
||||
logger.LogError(ex, "Failed to load configuration");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
catch (ConfigLoadException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error loading configuration: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,54 +163,59 @@ public static class ValidateCommand
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ValidateCommand");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var validationService = serviceProvider.GetRequiredService<IValidationService>();
|
||||
|
||||
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
|
||||
|
||||
try
|
||||
["Command"] = "validate pipelines",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
|
||||
var result = validationService.ValidatePipelines(pipelines);
|
||||
|
||||
if (!quiet)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
Console.WriteLine("=== Pipelines Validation ===");
|
||||
Console.WriteLine($"Directory: {pipelinesDir}");
|
||||
Console.WriteLine($"Pipelines found: {pipelines.Count}");
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine("Status: Valid");
|
||||
return 0;
|
||||
}
|
||||
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
|
||||
var validationService = serviceProvider.GetRequiredService<IValidationService>();
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {error}");
|
||||
}
|
||||
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
|
||||
|
||||
foreach (var warning in result.Warnings)
|
||||
try
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine($"Warning: {warning}");
|
||||
}
|
||||
var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
|
||||
var result = validationService.ValidatePipelines(pipelines);
|
||||
|
||||
return result.Errors.Count > 0 ? 1 : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error validating pipelines: {ex.Message}");
|
||||
return 1;
|
||||
logger.LogInformation("=== Pipelines Validation ===");
|
||||
logger.LogInformation("Directory: {Path}", pipelinesDir);
|
||||
logger.LogInformation("Pipelines found: {Count}", pipelines.Count);
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
logger.LogInformation("Status: Valid");
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
logger.LogError("{Error}", error);
|
||||
}
|
||||
|
||||
foreach (var warning in result.Warnings)
|
||||
{
|
||||
logger.LogWarning("{Warning}", warning);
|
||||
}
|
||||
|
||||
return result.Errors.Count > 0 ? 1 : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to validate pipelines");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,48 +225,54 @@ public static class ValidateCommand
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("ValidateCommand");
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var runtimeValidationService = serviceProvider.GetRequiredService<IRuntimeConfigValidationService>();
|
||||
|
||||
if (!quiet)
|
||||
["Command"] = "validate runtime",
|
||||
["ConfigPath"] = configPath ?? "(default)"
|
||||
}))
|
||||
{
|
||||
Console.WriteLine("=== Runtime Configuration Validation ===");
|
||||
Console.WriteLine($"Folder: {folderPath}");
|
||||
}
|
||||
|
||||
var results = runtimeValidationService.ValidateRuntimeConfig(folderPath);
|
||||
var hasErrors = false;
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (result.Errors.Count > 0)
|
||||
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
|
||||
if (folderPath == null)
|
||||
{
|
||||
hasErrors = true;
|
||||
Console.Error.WriteLine($"\n[{result.ValidatorName}]");
|
||||
foreach (var error in result.Errors)
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var runtimeValidationService = serviceProvider.GetRequiredService<IRuntimeConfigValidationService>();
|
||||
|
||||
logger.LogInformation("=== Runtime Configuration Validation ===");
|
||||
logger.LogInformation("Folder: {Path}", folderPath);
|
||||
|
||||
var results = runtimeValidationService.ValidateRuntimeConfig(folderPath);
|
||||
var hasErrors = false;
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (result.Errors.Count > 0)
|
||||
{
|
||||
Console.Error.WriteLine($" Error: {error}");
|
||||
hasErrors = true;
|
||||
logger.LogError("[{ValidatorName}]", result.ValidatorName);
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
logger.LogError(" {Error}", error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("[{ValidatorName}]: OK", result.ValidatorName);
|
||||
}
|
||||
|
||||
foreach (var warning in result.Warnings)
|
||||
{
|
||||
logger.LogWarning(" {Warning}", warning);
|
||||
}
|
||||
}
|
||||
else if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"[{result.ValidatorName}]: OK");
|
||||
}
|
||||
|
||||
foreach (var warning in result.Warnings)
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine($" Warning: {warning}");
|
||||
}
|
||||
return hasErrors ? 1 : 0;
|
||||
}
|
||||
|
||||
return hasErrors ? 1 : 0;
|
||||
}
|
||||
|
||||
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.*" />
|
||||
<PackageReference Include="Serilog" Version="4.*" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.*" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.*" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Cli.Commands;
|
||||
using JdeScoping.ConfigManager.Core.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli;
|
||||
|
||||
@@ -18,8 +19,12 @@ public static class Program
|
||||
/// <returns>Exit code: 0 for success, non-zero for errors.</returns>
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Pre-parse verbose/quiet flags for logging configuration
|
||||
bool verbose = args.Contains("-v") || args.Contains("--verbose");
|
||||
bool quiet = args.Contains("-q") || args.Contains("--quiet");
|
||||
|
||||
var services = new ServiceCollection();
|
||||
ConfigureServices(services);
|
||||
ConfigureServices(services, verbose, quiet);
|
||||
using var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
var rootCommand = new RootCommand("JDE Scoping Tool configuration management CLI")
|
||||
@@ -101,15 +106,34 @@ public static class Program
|
||||
connectionCommand.AddCommand(ConnectionCommands.CreateRemoveCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
rootCommand.AddCommand(connectionCommand);
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
try
|
||||
{
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureServices(IServiceCollection services)
|
||||
private static void ConfigureServices(IServiceCollection services, bool verbose, bool quiet)
|
||||
{
|
||||
services.AddLogging(builder => builder
|
||||
.AddConsole()
|
||||
.SetMinimumLevel(LogLevel.Warning));
|
||||
// Log level mapping:
|
||||
// --quiet: Warning level (hide success/info messages, only show warnings and errors)
|
||||
// default: Information level (show success messages)
|
||||
// --verbose: Debug level (show all messages including debug details)
|
||||
var logLevel = quiet ? LogEventLevel.Warning
|
||||
: verbose ? LogEventLevel.Debug
|
||||
: LogEventLevel.Information;
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Is(logLevel)
|
||||
.WriteTo.Console(
|
||||
outputTemplate: "{Message:lj}{NewLine}{Exception}")
|
||||
.Enrich.FromLogContext()
|
||||
.CreateLogger();
|
||||
|
||||
services.AddLogging(builder => builder.AddSerilog(dispose: true));
|
||||
services.AddConfigManagerCore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# ConfigManager CLI Logging Style Guide
|
||||
|
||||
This document describes the logging approach used in the ConfigManager CLI application.
|
||||
|
||||
## Architecture
|
||||
|
||||
The CLI uses **Microsoft.Extensions.Logging** as the logging abstraction with **Serilog** as the provider. This provides:
|
||||
|
||||
- Structured logging with semantic parameter names
|
||||
- Flexible log level control via command-line flags
|
||||
- Clean, message-only console output for CLI tooling
|
||||
|
||||
## Configuration
|
||||
|
||||
### Dependency Injection Setup
|
||||
|
||||
Logging is configured via dependency injection in `Program.cs`. Serilog is registered with the DI container:
|
||||
|
||||
```csharp
|
||||
private static void ConfigureServices(IServiceCollection services, bool verbose, bool quiet)
|
||||
{
|
||||
// Determine log level from command-line flags
|
||||
var logLevel = quiet ? LogEventLevel.Warning
|
||||
: verbose ? LogEventLevel.Debug
|
||||
: LogEventLevel.Information;
|
||||
|
||||
// Configure Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Is(logLevel)
|
||||
.WriteTo.Console(outputTemplate: "{Message:lj}{NewLine}{Exception}")
|
||||
.Enrich.FromLogContext()
|
||||
.CreateLogger();
|
||||
|
||||
// Register logging with DI container
|
||||
services.AddLogging(builder => builder.AddSerilog(dispose: true));
|
||||
|
||||
// Register other services...
|
||||
}
|
||||
```
|
||||
|
||||
This approach:
|
||||
- Creates a Serilog logger configured for CLI output
|
||||
- Registers it with `Microsoft.Extensions.Logging` via `AddSerilog()`
|
||||
- Makes `ILoggerFactory` available to command handlers through the service provider
|
||||
|
||||
### Log Level Mapping
|
||||
|
||||
The CLI supports three verbosity modes controlled by command-line flags:
|
||||
|
||||
| Flag | Log Level | Visible Messages |
|
||||
|------|-----------|------------------|
|
||||
| `--quiet` / `-q` | Warning | Warnings and errors only |
|
||||
| (default) | Information | Success messages, data output, warnings, errors |
|
||||
| `--verbose` / `-v` | Debug | All messages including debug details |
|
||||
|
||||
### Output Format
|
||||
|
||||
The Serilog output template is configured for clean CLI output:
|
||||
|
||||
```
|
||||
{Message:lj}{NewLine}{Exception}
|
||||
```
|
||||
|
||||
This produces message-only output without timestamps or log levels, suitable for command-line tools.
|
||||
|
||||
## Logger Injection Pattern
|
||||
|
||||
Command handlers obtain a logger from the service provider:
|
||||
|
||||
```csharp
|
||||
private static async Task<int> SomeCommandAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("CommandClassName");
|
||||
|
||||
// Command implementation using logger
|
||||
}
|
||||
```
|
||||
|
||||
## Structured Logging Patterns
|
||||
|
||||
### Log Scope with Context Properties
|
||||
|
||||
Wrap command logic in a log scope to add context:
|
||||
|
||||
```csharp
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["Command"] = "secret set",
|
||||
["ConfigPath"] = configPath ?? "(default)",
|
||||
["SecretKey"] = key
|
||||
}))
|
||||
{
|
||||
// All log messages within this block include the scope properties
|
||||
}
|
||||
```
|
||||
|
||||
### Standard Context Properties
|
||||
|
||||
| Property | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `Command` | Full command path | `"secret list"`, `"config set datasync"` |
|
||||
| `ConfigPath` | Configuration folder path | `"/app/config"` or `"(default)"` |
|
||||
| `ConnectionName` | Connection name for connection commands | `"jde"` |
|
||||
| `PipelineName` | Pipeline name for pipeline commands | `"WorkOrder"` |
|
||||
| `BackupTimestamp` | Timestamp for backup operations | `"2026-01-15_120000"` |
|
||||
|
||||
### Message Templates
|
||||
|
||||
Use semantic parameter names in message templates:
|
||||
|
||||
```csharp
|
||||
// Good - uses named parameters
|
||||
logger.LogInformation("Secret '{Key}' set successfully", key);
|
||||
logger.LogError("Connection '{Name}' not found", name);
|
||||
|
||||
// Avoid - positional or embedded values
|
||||
logger.LogInformation($"Secret '{key}' set successfully");
|
||||
```
|
||||
|
||||
## Log Level Guidelines
|
||||
|
||||
### Information Level
|
||||
|
||||
Use for:
|
||||
- Success messages
|
||||
- Data output (lists, tables, JSON)
|
||||
- Section headers
|
||||
|
||||
```csharp
|
||||
logger.LogInformation("Secret '{Key}' set successfully", key);
|
||||
logger.LogInformation("=== Connections ({Count}) ===", entries.Count);
|
||||
logger.LogInformation("{Data}", JsonSerializer.Serialize(section, options));
|
||||
```
|
||||
|
||||
### Debug Level
|
||||
|
||||
Use for:
|
||||
- Verbose details (shown with `--verbose`)
|
||||
- Intermediate progress updates
|
||||
- Detailed configuration values
|
||||
|
||||
```csharp
|
||||
logger.LogDebug("Backup created: {BackupName}", Path.GetFileName(backupPath));
|
||||
logger.LogDebug("LookbackMultiplier: {Value}", section.LookbackMultiplier);
|
||||
```
|
||||
|
||||
### Warning Level
|
||||
|
||||
Use for:
|
||||
- Non-critical issues
|
||||
- Validation warnings
|
||||
|
||||
```csharp
|
||||
logger.LogWarning("{Warning}", warning);
|
||||
```
|
||||
|
||||
### Error Level
|
||||
|
||||
Use for:
|
||||
- Operation failures
|
||||
- Missing required files
|
||||
- Validation errors
|
||||
- Exceptions
|
||||
|
||||
```csharp
|
||||
logger.LogError("Could not find configuration folder. Use --config-path to specify");
|
||||
logger.LogError("Connection '{Name}' not found", name);
|
||||
logger.LogError(ex, "Failed to set secret");
|
||||
```
|
||||
|
||||
## Converting from Console.WriteLine
|
||||
|
||||
| Pattern | Before | After |
|
||||
|---------|--------|-------|
|
||||
| Success message | `Console.WriteLine($"Secret '{key}' set successfully");` | `logger.LogInformation("Secret '{Key}' set successfully", key);` |
|
||||
| Error message | `Console.Error.WriteLine($"Error: {ex.Message}");` | `logger.LogError(ex, "Operation failed");` |
|
||||
| Data output | `Console.WriteLine(JsonSerializer.Serialize(...));` | `logger.LogInformation("{Data}", JsonSerializer.Serialize(...));` |
|
||||
| Table row | `Console.WriteLine($"{name,-25} {value,-15}");` | `logger.LogInformation("{Name,-25} {Value,-15}", name, value);` |
|
||||
| Verbose detail | `if (verbose) Console.WriteLine(...);` | `logger.LogDebug(...);` |
|
||||
| Quiet guard | `if (!quiet) Console.WriteLine(...);` | `logger.LogInformation(...);` (level handles filtering) |
|
||||
|
||||
## Interactive Prompts
|
||||
|
||||
Keep `Console.Write` for interactive prompts that require user input:
|
||||
|
||||
```csharp
|
||||
// This stays as Console.Write - interactive prompt
|
||||
Console.Write($"Delete pipeline '{name}'? [y/N] ");
|
||||
var response = Console.ReadLine();
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use structured parameters** - Never embed values directly in message strings
|
||||
2. **Use consistent context properties** - Follow the standard property names above
|
||||
3. **Let log levels handle filtering** - Don't add manual `if (!quiet)` guards
|
||||
4. **Include exceptions in error logs** - Use `logger.LogError(ex, "Message")` to capture stack traces
|
||||
5. **Keep messages actionable** - Error messages should help users understand what to do next
|
||||
@@ -23,6 +23,7 @@ public class BackupCommandsTests
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_backupService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddTestLogging();
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
@@ -272,10 +273,10 @@ public class BackupCommandsTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -288,7 +289,7 @@ public class BackupCommandsTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -27,6 +27,7 @@ public class ConfigCommandsTests
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddSingleton(_backupService);
|
||||
services.AddTestLogging();
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
@@ -366,10 +367,10 @@ public class ConfigCommandsTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -382,7 +383,7 @@ public class ConfigCommandsTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
+17
-16
@@ -24,6 +24,7 @@ public class ConnectionCommandsTests
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddTestLogging();
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
@@ -219,10 +220,10 @@ public class ConnectionCommandsTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -235,7 +236,7 @@ public class ConnectionCommandsTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -331,10 +332,10 @@ public class ConnectionCommandsTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -347,7 +348,7 @@ public class ConnectionCommandsTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -393,10 +394,10 @@ public class ConnectionCommandsTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -409,7 +410,7 @@ public class ConnectionCommandsTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -478,10 +479,10 @@ public class ConnectionCommandsTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -494,7 +495,7 @@ public class ConnectionCommandsTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -24,6 +24,7 @@ public class PipelineCommandsTests
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddTestLogging();
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
|
||||
@@ -28,6 +28,7 @@ public class SecretCommandsTests
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddSingleton(_secureStoreManager);
|
||||
services.AddTestLogging();
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
@@ -169,10 +170,10 @@ public class SecretCommandsTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -185,7 +186,7 @@ public class SecretCommandsTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
+5
-4
@@ -27,6 +27,7 @@ public class TestConnectionCommandTests
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddSingleton(_connectionTestService);
|
||||
services.AddTestLogging();
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
@@ -243,10 +244,10 @@ public class TestConnectionCommandTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -259,7 +260,7 @@ public class TestConnectionCommandTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -31,6 +31,7 @@ public class ValidateCommandTests
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddSingleton(_validationService);
|
||||
services.AddSingleton(_runtimeValidationService);
|
||||
services.AddTestLogging();
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
@@ -193,10 +194,10 @@ public class ValidateCommandTests
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
// Capture console output (Serilog writes all output to stdout)
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -205,11 +206,11 @@ public class ValidateCommandTests
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("Error");
|
||||
output.ShouldContain("Connection string 'LocalCache' is required");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for setting up logging in CLI tests.
|
||||
/// </summary>
|
||||
public static class TestLoggingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Serilog logging to a service collection configured to write to Console.
|
||||
/// This allows tests to capture log output via Console.SetOut.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddTestLogging(this IServiceCollection services, LogEventLevel level = LogEventLevel.Information)
|
||||
{
|
||||
var logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Is(level)
|
||||
.WriteTo.Console(outputTemplate: "{Message:lj}{NewLine}{Exception}")
|
||||
.CreateLogger();
|
||||
|
||||
services.AddLogging(builder => builder.AddSerilog(logger, dispose: true));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user