Added container rebuild flow with real-time logs and updated UI, backend implementation, config options, and container helper API integration.
This commit is contained in:
7
Moonlight.Api/Configuration/ContainerHelperOptions.cs
Normal file
7
Moonlight.Api/Configuration/ContainerHelperOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class ContainerHelperOptions
|
||||
{
|
||||
public bool IsEnabled { get; set; }
|
||||
public string Url { get; set; } = "http://helper:8080";
|
||||
}
|
||||
27
Moonlight.Api/Http/Controllers/Admin/ChController.cs
Normal file
27
Moonlight.Api/Http/Controllers/Admin/ChController.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.Api.Services;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/ch")]
|
||||
public class ChController : Controller
|
||||
{
|
||||
private readonly ContainerHelperService ContainerHelperService;
|
||||
|
||||
public ChController(ContainerHelperService containerHelperService)
|
||||
{
|
||||
ContainerHelperService = containerHelperService;
|
||||
}
|
||||
|
||||
[HttpPost("rebuild")]
|
||||
public Task<IResult> RebuildAsync()
|
||||
{
|
||||
var result = ContainerHelperService.RebuildAsync();
|
||||
|
||||
return Task.FromResult<IResult>(
|
||||
TypedResults.ServerSentEvents(result)
|
||||
);
|
||||
}
|
||||
}
|
||||
13
Moonlight.Api/Http/Controllers/PingController.cs
Normal file
13
Moonlight.Api/Http/Controllers/PingController.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/ping")]
|
||||
public class PingController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Get() => Ok("Pong");
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class ApplicationService : IHostedLifecycleService
|
||||
// TODO: Update / version check
|
||||
|
||||
VersionName = "v2.1.0 (a2d4edc0e5)";
|
||||
IsUpToDate = true;
|
||||
IsUpToDate = false;
|
||||
|
||||
OperatingSystem = OsHelper.GetName();
|
||||
}
|
||||
|
||||
67
Moonlight.Api/Services/ContainerHelperService.cs
Normal file
67
Moonlight.Api/Services/ContainerHelperService.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Text.Json;
|
||||
using Moonlight.Shared.Http;
|
||||
using Moonlight.Shared.Http.Events;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
public class ContainerHelperService
|
||||
{
|
||||
private readonly IHttpClientFactory HttpClientFactory;
|
||||
|
||||
public ContainerHelperService(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
HttpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<RebuildEvent> RebuildAsync()
|
||||
{
|
||||
var options = new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
options.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
|
||||
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||
|
||||
var response = await client.GetAsync("api/rebuild", HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
yield return new RebuildEvent()
|
||||
{
|
||||
Type = RebuildEventType.Failed,
|
||||
Data = responseText
|
||||
};
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
using var streamReader = new StreamReader(responseStream);
|
||||
|
||||
do
|
||||
{
|
||||
var line = await streamReader.ReadLineAsync();
|
||||
|
||||
if(line == null)
|
||||
break;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var data = line.Trim("data: ");
|
||||
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEvent>(data, options);
|
||||
|
||||
yield return deserializedData;
|
||||
} while (true);
|
||||
|
||||
yield return new RebuildEvent()
|
||||
{
|
||||
Type = RebuildEventType.Succeeded,
|
||||
Data = string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,15 @@ public partial class Startup
|
||||
|
||||
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
||||
builder.Services.AddScoped<FrontendService>();
|
||||
|
||||
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
|
||||
builder.Services.AddSingleton<ContainerHelperService>();
|
||||
|
||||
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
|
||||
{
|
||||
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
|
||||
client.BaseAddress = new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
|
||||
});
|
||||
}
|
||||
|
||||
private static void UseBase(WebApplication application)
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@using System.Text.Json
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Events
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Progresses
|
||||
@using ShadcnBlazor.Spinners
|
||||
|
||||
@inject AlertDialogService AlertService
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Updating...
|
||||
Updating instance...
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="text-base flex flex-col p-2 gap-y-0.5">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 w-full gap-5">
|
||||
<div class="text-base flex flex-col p-2 gap-y-0.5">
|
||||
@for (var i = 0; i < Steps.Length; i++)
|
||||
{
|
||||
if (CurrentStep == i)
|
||||
{
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<Spinner ClassName="size-4" />
|
||||
<Spinner ClassName="size-4"/>
|
||||
<span>
|
||||
@Steps[i]
|
||||
</span>
|
||||
@@ -31,7 +36,7 @@
|
||||
if (i < CurrentStep)
|
||||
{
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<CheckIcon ClassName="text-green-500 size-4" />
|
||||
<CheckIcon ClassName="text-green-500 size-4"/>
|
||||
<span>
|
||||
@Steps[i]
|
||||
</span>
|
||||
@@ -46,6 +51,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="bg-black text-white rounded-lg font-mono h-96 flex flex-col-reverse overflow-auto p-3 scrollbar-thin">
|
||||
@for (var i = LogLines.Count - 1; i >= 0; i--)
|
||||
{
|
||||
<div>
|
||||
@LogLines[i]
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
@@ -62,12 +76,15 @@
|
||||
[
|
||||
"Preparing",
|
||||
"Updating configuration files",
|
||||
"Starting rebuild task",
|
||||
"Building docker image",
|
||||
"Redeploying container instance",
|
||||
"Waiting for container instance to start up",
|
||||
"Update complete"
|
||||
];
|
||||
|
||||
private List<string?> LogLines = new();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender)
|
||||
@@ -83,27 +100,97 @@
|
||||
Progress = 20;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(6000);
|
||||
|
||||
CurrentStep = 2;
|
||||
Progress = 40;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(2000);
|
||||
|
||||
CurrentStep = 3;
|
||||
Progress = 60;
|
||||
CurrentStep = 2;
|
||||
Progress = 30;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(4000);
|
||||
var response = await HttpClient.SendAsync(
|
||||
new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild"),
|
||||
HttpCompletionOption.ResponseHeadersRead
|
||||
);
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
using var streamReader = new StreamReader(responseStream);
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
var line = await streamReader.ReadLineAsync();
|
||||
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var data = line.Trim("data: ");
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEvent>(data, Constants.SerializerOptions);
|
||||
|
||||
switch (deserializedData.Type)
|
||||
{
|
||||
case RebuildEventType.Log:
|
||||
LogLines.Add(deserializedData.Data);
|
||||
break;
|
||||
|
||||
case RebuildEventType.Step:
|
||||
|
||||
switch (deserializedData.Data)
|
||||
{
|
||||
case "BuildImage":
|
||||
CurrentStep = 3;
|
||||
Progress = 40;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
break;
|
||||
|
||||
case "ServiceDown":
|
||||
CurrentStep = 4;
|
||||
Progress = 60;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
break;
|
||||
|
||||
case "ServiceUp":
|
||||
CurrentStep = 4;
|
||||
Progress = 80;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(4000);
|
||||
break;
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// TODO: Log
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
|
||||
CurrentStep = 5;
|
||||
Progress = 90;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// Ping instance until its reachable again
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await HttpClient.GetStringAsync("api/ping");
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
await Task.Delay(3000);
|
||||
}
|
||||
|
||||
CurrentStep = 6;
|
||||
Progress = 100;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
|
||||
@@ -160,5 +160,6 @@
|
||||
private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync<UpdateInstanceModal>(onConfigure: model =>
|
||||
{
|
||||
model.ShowCloseButton = false;
|
||||
model.ClassName = "sm:max-w-4xl!";
|
||||
});
|
||||
}
|
||||
|
||||
20
Moonlight.Shared/Http/Events/RebuildEvent.cs
Normal file
20
Moonlight.Shared/Http/Events/RebuildEvent.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Moonlight.Shared.Http.Events;
|
||||
|
||||
public struct RebuildEvent
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public RebuildEventType Type { get; set; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public enum RebuildEventType
|
||||
{
|
||||
Log = 0,
|
||||
Failed = 1,
|
||||
Succeeded = 2,
|
||||
Step = 3
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Shared.Http.Events;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Themes;
|
||||
@@ -43,6 +44,9 @@ namespace Moonlight.Shared.Http;
|
||||
[JsonSerializable(typeof(UpdateThemeDto))]
|
||||
[JsonSerializable(typeof(PagedData<ThemeDto>))]
|
||||
[JsonSerializable(typeof(ThemeDto))]
|
||||
|
||||
// Events
|
||||
[JsonSerializable(typeof(RebuildEvent))]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
28
compose.yaml
28
compose.yaml
@@ -37,3 +37,31 @@
|
||||
# Logging
|
||||
- "Logging__LogLevel__Default=Information"
|
||||
- "Logging__LogLevel__Microsoft.AspNetCore=Warning"
|
||||
- "Logging__LogLevel__System.Net.Http.HttpClient=Warning"
|
||||
|
||||
- "Moonlight__ContainerHelper__IsEnabled=true"
|
||||
- "Moonlight__ContainerHelper__Url=http://app:8080"
|
||||
|
||||
app:
|
||||
image: git.battlestati.one/moonlight-panel/container_helper
|
||||
|
||||
group_add:
|
||||
- "989"
|
||||
|
||||
environment:
|
||||
# Logging
|
||||
- "Logging__LogLevel__Default=Information"
|
||||
- "Logging__LogLevel__Microsoft.AspNetCore=Warning"
|
||||
|
||||
# Compose
|
||||
- "ContainerHelper__Compose__Directory=${PWD}"
|
||||
- "ContainerHelper__Compose__Binary=docker-compose"
|
||||
- "ContainerHelper__Service__Name=api"
|
||||
|
||||
# HTTP Proxy
|
||||
- "HTTP_PROXY=${HTTP_PROXY}"
|
||||
- "HTTPS_PROXY=${HTTPS_PROXY}"
|
||||
|
||||
volumes:
|
||||
- "${PWD}:${PWD}"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
Reference in New Issue
Block a user