Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
- Implemented PqSoftCryptoProvider for software-only post-quantum algorithms (Dilithium3, Falcon512) using BouncyCastle. - Added PqSoftProviderOptions and PqSoftKeyOptions for configuration. - Created unit tests for Dilithium3 and Falcon512 signing and verification. - Introduced EcdsaPolicyCryptoProvider for compliance profiles (FIPS/eIDAS) with explicit allow-lists. - Added KcmvpHashOnlyProvider for KCMVP baseline compliance. - Updated project files and dependencies for new libraries and testing frameworks.
240 lines
8.7 KiB
C#
240 lines
8.7 KiB
C#
using System;
|
|
using System.CommandLine;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StellaOps.Cli.Services;
|
|
using StellaOps.Cli.Extensions;
|
|
using StellaOps.Infrastructure.Postgres.Migrations;
|
|
|
|
namespace StellaOps.Cli.Commands;
|
|
|
|
internal static class SystemCommandBuilder
|
|
{
|
|
private static MigrationCategory? ParseCategory(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return value.ToLowerInvariant() switch
|
|
{
|
|
"startup" => MigrationCategory.Startup,
|
|
"release" => MigrationCategory.Release,
|
|
"seed" => MigrationCategory.Seed,
|
|
"data" => MigrationCategory.Data,
|
|
_ => throw new CommandLineException("Unknown category. Expected: startup|release|seed|data")
|
|
};
|
|
}
|
|
|
|
internal static Command BuildSystemCommand(
|
|
IServiceProvider services,
|
|
Option<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var moduleOption = new Option<string?>("--module")
|
|
{
|
|
Description = "Module name (Authority, Scheduler, Concelier, Policy, Notify, Excititor, all)"
|
|
};
|
|
var categoryOption = new Option<string?>("--category")
|
|
{
|
|
Description = "Migration category (startup, release, seed, data)"
|
|
};
|
|
var dryRunOption = new Option<bool>("--dry-run")
|
|
{
|
|
Description = "List migrations without executing"
|
|
};
|
|
var connectionOption = new Option<string?>("--connection")
|
|
{
|
|
Description = "PostgreSQL connection string override (otherwise uses STELLAOPS_POSTGRES_* env vars)"
|
|
};
|
|
var timeoutOption = new Option<int?>("--timeout")
|
|
{
|
|
Description = "Command timeout in seconds for each migration (default 300)."
|
|
};
|
|
var forceOption = new Option<bool>("--force")
|
|
{
|
|
Description = "Allow execution of release migrations without --dry-run."
|
|
};
|
|
|
|
var run = new Command("migrations-run", "Run migrations for the selected module(s).");
|
|
run.Add(moduleOption);
|
|
run.Add(categoryOption);
|
|
run.Add(dryRunOption);
|
|
run.Add(connectionOption);
|
|
run.Add(timeoutOption);
|
|
run.Add(forceOption);
|
|
run.SetAction(async parseResult =>
|
|
{
|
|
var modules = MigrationModuleRegistry.GetModules(parseResult.GetValue(moduleOption)).ToList();
|
|
if (!modules.Any())
|
|
{
|
|
throw new CommandLineException(
|
|
"No modules matched the filter; available: " + string.Join(", ", MigrationModuleRegistry.ModuleNames));
|
|
}
|
|
|
|
var category = ParseCategory(parseResult.GetValue(categoryOption));
|
|
var dryRun = parseResult.GetValue(dryRunOption);
|
|
var force = parseResult.GetValue(forceOption);
|
|
|
|
if (category == MigrationCategory.Release && !dryRun && !force)
|
|
{
|
|
throw new CommandLineException(
|
|
"Release migrations require explicit approval; use --dry-run to preview or --force to execute.");
|
|
}
|
|
|
|
var connection = parseResult.GetValue(connectionOption);
|
|
var timeoutSeconds = parseResult.GetValue(timeoutOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
var migrationService = services.GetRequiredService<MigrationCommandService>();
|
|
|
|
foreach (var module in modules)
|
|
{
|
|
var result = await migrationService
|
|
.RunAsync(module, connection, category, dryRun, timeoutSeconds, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
WriteRunResult(module, result, verbose);
|
|
}
|
|
});
|
|
|
|
var status = new Command("migrations-status", "Show migration status for the selected module(s).");
|
|
status.Add(moduleOption);
|
|
status.Add(connectionOption);
|
|
status.SetAction(async parseResult =>
|
|
{
|
|
var modules = MigrationModuleRegistry.GetModules(parseResult.GetValue(moduleOption)).ToList();
|
|
if (!modules.Any())
|
|
{
|
|
throw new CommandLineException(
|
|
"No modules matched the filter; available: " + string.Join(", ", MigrationModuleRegistry.ModuleNames));
|
|
}
|
|
|
|
var connection = parseResult.GetValue(connectionOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
var migrationService = services.GetRequiredService<MigrationCommandService>();
|
|
|
|
foreach (var module in modules)
|
|
{
|
|
var statusResult = await migrationService
|
|
.GetStatusAsync(module, connection, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
WriteStatusResult(module, statusResult, verbose);
|
|
}
|
|
});
|
|
|
|
var verify = new Command("migrations-verify", "Verify migration checksums for the selected module(s).");
|
|
verify.Add(moduleOption);
|
|
verify.Add(connectionOption);
|
|
verify.SetAction(async parseResult =>
|
|
{
|
|
var modules = MigrationModuleRegistry.GetModules(parseResult.GetValue(moduleOption)).ToList();
|
|
if (!modules.Any())
|
|
{
|
|
throw new CommandLineException(
|
|
"No modules matched the filter; available: " + string.Join(", ", MigrationModuleRegistry.ModuleNames));
|
|
}
|
|
|
|
var connection = parseResult.GetValue(connectionOption);
|
|
var migrationService = services.GetRequiredService<MigrationCommandService>();
|
|
|
|
foreach (var module in modules)
|
|
{
|
|
var errors = await migrationService
|
|
.VerifyAsync(module, connection, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
WriteVerifyResult(module, errors);
|
|
}
|
|
});
|
|
|
|
var system = new Command("system", "System operations (migrations).");
|
|
system.Add(run);
|
|
system.Add(status);
|
|
system.Add(verify);
|
|
return system;
|
|
}
|
|
|
|
private static void WriteRunResult(MigrationModuleInfo module, MigrationResult result, bool verbose)
|
|
{
|
|
var prefix = $"[{module.Name}]";
|
|
|
|
if (!result.Success)
|
|
{
|
|
Console.Error.WriteLine($"{prefix} FAILED: {result.ErrorMessage}");
|
|
foreach (var error in result.ChecksumErrors)
|
|
{
|
|
Console.Error.WriteLine($"{prefix} checksum: {error}");
|
|
}
|
|
|
|
if (Environment.ExitCode == 0)
|
|
{
|
|
Environment.ExitCode = 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
Console.WriteLine(
|
|
$"{prefix} applied={result.AppliedCount} skipped={result.SkippedCount} filtered={result.FilteredCount} duration_ms={result.DurationMs}");
|
|
|
|
if (verbose && result.AppliedMigrations.Count > 0)
|
|
{
|
|
foreach (var migration in result.AppliedMigrations.OrderBy(m => m.Name))
|
|
{
|
|
var mode = migration.WasDryRun ? "DRY-RUN" : "APPLIED";
|
|
Console.WriteLine($"{prefix} {mode}: {migration.Name} ({migration.Category}) {migration.DurationMs}ms");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void WriteStatusResult(MigrationModuleInfo module, MigrationStatus status, bool verbose)
|
|
{
|
|
var prefix = $"[{module.Name}]";
|
|
|
|
Console.WriteLine(
|
|
$"{prefix} applied={status.AppliedCount} pending_startup={status.PendingStartupCount} pending_release={status.PendingReleaseCount} checksum_errors={status.ChecksumErrors.Count}");
|
|
|
|
if (verbose)
|
|
{
|
|
foreach (var pending in status.PendingMigrations.OrderBy(p => p.Name))
|
|
{
|
|
Console.WriteLine($"{prefix} pending {pending.Category}: {pending.Name}");
|
|
}
|
|
|
|
foreach (var error in status.ChecksumErrors)
|
|
{
|
|
Console.WriteLine($"{prefix} checksum: {error}");
|
|
}
|
|
}
|
|
|
|
if (status.HasBlockingIssues && Environment.ExitCode == 0)
|
|
{
|
|
Environment.ExitCode = 1;
|
|
}
|
|
}
|
|
|
|
private static void WriteVerifyResult(MigrationModuleInfo module, IReadOnlyList<string> errors)
|
|
{
|
|
var prefix = $"[{module.Name}]";
|
|
|
|
if (errors.Count == 0)
|
|
{
|
|
Console.WriteLine($"{prefix} checksum verification passed.");
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"{prefix} checksum verification failed ({errors.Count}).");
|
|
foreach (var error in errors)
|
|
{
|
|
Console.Error.WriteLine($"{prefix} {error}");
|
|
}
|
|
|
|
if (Environment.ExitCode == 0)
|
|
{
|
|
Environment.ExitCode = 1;
|
|
}
|
|
}
|
|
}
|