Program.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. using System.Text.Json.Serialization;
  2. using Microsoft.AspNetCore.Components;
  3. using Microsoft.AspNetCore.DataProtection;
  4. using Microsoft.AspNetCore.Hosting.StaticWebAssets;
  5. using RackPeek.Domain;
  6. using RackPeek.Domain.Git;
  7. using RackPeek.Domain.Persistence;
  8. using RackPeek.Domain.Persistence.Yaml;
  9. using RackPeek.Web.Api;
  10. using RackPeek.Web.Components;
  11. using Shared.Rcl;
  12. using Shared.Rcl.Docs;
  13. using Shared.Rcl.Servers;
  14. namespace RackPeek.Web;
  15. public class Program {
  16. public static async Task<WebApplication> BuildApp(WebApplicationBuilder builder) {
  17. StaticWebAssetsLoader.UseStaticWebAssets(
  18. builder.Environment,
  19. builder.Configuration
  20. );
  21. var yamlDir = builder.Configuration.GetValue<string>("RPK_YAML_DIR") ?? "./config";
  22. var yamlFileName = "config.yaml";
  23. var basePath = Directory.GetCurrentDirectory();
  24. var yamlPath = Path.IsPathRooted(yamlDir)
  25. ? yamlDir
  26. : Path.Combine(basePath, yamlDir);
  27. Directory.CreateDirectory(yamlPath);
  28. var yamlFilePath = Path.Combine(yamlPath, yamlFileName);
  29. if (!File.Exists(yamlFilePath)) {
  30. try {
  31. await using var fs = new FileStream(
  32. yamlFilePath,
  33. FileMode.CreateNew,
  34. FileAccess.Write,
  35. FileShare.None);
  36. await using var writer = new StreamWriter(fs);
  37. await writer.WriteLineAsync("# default config");
  38. }
  39. catch (IOException) when (File.Exists(yamlFilePath)) {
  40. // Another instance created the file between the existence
  41. // check and CreateNew — the config is there, carry on.
  42. }
  43. }
  44. // Persist DataProtection keys next to the config so they live on the
  45. // mounted volume: they survive container recreation, and key writes
  46. // no longer depend on a writable user profile or /tmp — both of
  47. // which are unavailable in hardened Docker setups (#312).
  48. var keysPath = Path.Combine(yamlPath, ".dataprotection");
  49. Directory.CreateDirectory(keysPath);
  50. builder.Services.AddDataProtection()
  51. .PersistKeysToFileSystem(new DirectoryInfo(keysPath))
  52. .SetApplicationName("RackPeek");
  53. builder.Services.ConfigureHttpJsonOptions(options => {
  54. options.SerializerOptions.Converters.Add(
  55. new JsonStringEnumConverter());
  56. });
  57. builder.Services.AddScoped<ITextFileStore, PhysicalTextFileStore>();
  58. builder.Services.AddScoped<IDocsContentProvider, StaticWebAssetDocsContentProvider>();
  59. builder.Services.AddGitServices(builder.Configuration, yamlPath);
  60. var resources = new ResourceCollection();
  61. builder.Services.AddSingleton(resources);
  62. builder.Services.AddScoped<RackPeekConfigMigrationDeserializer>();
  63. builder.Services.AddScoped<IResourceYamlMigrationService, ResourceYamlMigrationService>();
  64. builder.Services.AddScoped<IResourceCollection>(sp =>
  65. new YamlResourceCollection(
  66. yamlFilePath,
  67. sp.GetRequiredService<ITextFileStore>(),
  68. sp.GetRequiredService<ResourceCollection>(),
  69. sp.GetRequiredService<IResourceYamlMigrationService>()));
  70. // Infrastructure
  71. builder.Services.AddYamlRepos();
  72. builder.Services.AddUseCases();
  73. builder.Services.AddCommands();
  74. builder.Services.AddScoped<IConsoleEmulator, ConsoleEmulator>();
  75. // Razor Components
  76. builder.Services.AddRazorComponents()
  77. .AddInteractiveServerComponents();
  78. WebApplication app = builder.Build();
  79. // Read the config into memory before anything can be served. Blazor reloads it
  80. // on every circuit init, but the inventory API has no circuit — without this it
  81. // would merge against an empty collection and persist that over the user's file,
  82. // destroying the inventory on the first request after a restart.
  83. await using (AsyncServiceScope scope = app.Services.CreateAsyncScope()) {
  84. try {
  85. await scope.ServiceProvider.GetRequiredService<IResourceCollection>().LoadAsync();
  86. }
  87. catch (Exception ex) {
  88. // An unreadable config must not stop the server booting: the web UI is
  89. // how someone fixes it, and a container that will not start is worse
  90. // than one showing the error. Blazor surfaces it on the first page load,
  91. // and every write path re-checks the load before persisting anything,
  92. // so booting in this state cannot overwrite the file.
  93. scope.ServiceProvider.GetRequiredService<ILogger<Program>>()
  94. .LogError(ex, "Could not read the config at {Path}. Fix it in the web UI.", yamlFilePath);
  95. }
  96. }
  97. if (!app.Environment.IsDevelopment()) {
  98. app.UseExceptionHandler("/Error");
  99. app.UseHsts();
  100. }
  101. app.UseHttpsRedirection();
  102. app.UseStaticFiles();
  103. app.UseRouting();
  104. app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
  105. app.UseAntiforgery();
  106. app.MapInventoryApi();
  107. app.MapStaticAssets();
  108. app.MapRazorComponents<App>()
  109. .AddInteractiveServerRenderMode()
  110. .AddAdditionalAssemblies(typeof(ServersListPage).Assembly);
  111. return app;
  112. }
  113. public static async Task Main(string[] args) {
  114. WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
  115. WebApplication app = await BuildApp(builder);
  116. await app.RunAsync();
  117. }
  118. }