DiscoveryOutput.cs 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. using RackPeek.Domain.Api;
  2. using RackPeek.Domain.Discovery;
  3. using RackPeek.Domain.Resources;
  4. using Spectre.Console;
  5. namespace Shared.Rcl.Commands.Discovery;
  6. /// <summary>
  7. /// The one place a discovery result leaves the process: printed as YAML, or sent to
  8. /// a server. Shared so every collector behaves identically.
  9. /// </summary>
  10. public static class DiscoveryOutput {
  11. public static async Task<int> EmitAsync(
  12. IReadOnlyList<Resource> resources,
  13. DiscoverSettings settings,
  14. CancellationToken cancellationToken) {
  15. if (resources.Count == 0) {
  16. AnsiConsole.MarkupLine("[yellow]Nothing discovered.[/]");
  17. return 0;
  18. }
  19. var yaml = DiscoveryDocument.ToYaml(resources);
  20. if (!settings.ShouldUpload) {
  21. // Raw write, not through Spectre's renderer: this is meant to be redirected
  22. // to a file, and the renderer word-wraps lines longer than the console
  23. // width — which splits a long single-token scalar and corrupts the YAML.
  24. // The active console's own writer keeps the web console emulator working.
  25. await AnsiConsole.Console.Profile.Out.Writer.WriteLineAsync(yaml);
  26. return 0;
  27. }
  28. return await UploadAsync(yaml, settings, cancellationToken);
  29. }
  30. private static async Task<int> UploadAsync(
  31. string yaml,
  32. DiscoverSettings settings,
  33. CancellationToken cancellationToken) {
  34. var server = settings.ResolvedServer;
  35. var apiKey = settings.ResolvedApiKey;
  36. if (string.IsNullOrWhiteSpace(server) || string.IsNullOrWhiteSpace(apiKey)) {
  37. // Validate() enforces both; this is the guard for a caller that skipped it.
  38. AnsiConsole.MarkupLine(
  39. "[red]No server or API key. Pass --server and --api-key, or set RPK_SERVER and RPK_API_KEY.[/]");
  40. return 1;
  41. }
  42. try {
  43. using var publisher = new DiscoveryPublisher(server, apiKey);
  44. ImportYamlResponse response = await publisher.PublishAsync(yaml, settings.DryRun, cancellationToken);
  45. Report(response, settings.DryRun, server);
  46. return 0;
  47. }
  48. catch (Exception ex) when (
  49. ex is InvalidOperationException or HttpRequestException or UriFormatException
  50. // HttpClient reports its own timeout as a cancellation.
  51. || (ex is TaskCanceledException && !cancellationToken.IsCancellationRequested)) {
  52. AnsiConsole.MarkupLine($"[red]{Markup.Escape(ex.Message)}[/]");
  53. return 1;
  54. }
  55. }
  56. private static void Report(ImportYamlResponse response, bool dryRun, string server) {
  57. List(response.Added, "added", "green");
  58. List(response.Updated, "updated", "yellow");
  59. List(response.Replaced, "replaced", "yellow");
  60. var total = response.Added.Count + response.Updated.Count + response.Replaced.Count;
  61. if (total == 0) {
  62. AnsiConsole.MarkupLine($"[grey]No changes — {Markup.Escape(server)} is already up to date.[/]");
  63. return;
  64. }
  65. AnsiConsole.MarkupLine(dryRun
  66. ? $"[grey]Dry run — nothing was written to {Markup.Escape(server)}.[/]"
  67. : $"[grey]{total} resource(s) written to {Markup.Escape(server)}.[/]");
  68. }
  69. private static void List(IReadOnlyList<string> names, string label, string colour) {
  70. foreach (var name in names)
  71. AnsiConsole.MarkupLine($"[{colour}]{label}[/] {Markup.Escape(name)}");
  72. }
  73. }