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
|
// TODO: Update / version check
|
||||||
|
|
||||||
VersionName = "v2.1.0 (a2d4edc0e5)";
|
VersionName = "v2.1.0 (a2d4edc0e5)";
|
||||||
IsUpToDate = true;
|
IsUpToDate = false;
|
||||||
|
|
||||||
OperatingSystem = OsHelper.GetName();
|
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.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
||||||
builder.Services.AddScoped<FrontendService>();
|
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)
|
private static void UseBase(WebApplication application)
|
||||||
|
|||||||
@@ -1,37 +1,31 @@
|
|||||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||||
|
|
||||||
|
@using System.Text.Json
|
||||||
@using LucideBlazor
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Shared.Http
|
||||||
|
@using Moonlight.Shared.Http.Events
|
||||||
@using ShadcnBlazor.Dialogs
|
@using ShadcnBlazor.Dialogs
|
||||||
@using ShadcnBlazor.Extras.AlertDialogs
|
@using ShadcnBlazor.Extras.AlertDialogs
|
||||||
@using ShadcnBlazor.Progresses
|
@using ShadcnBlazor.Progresses
|
||||||
@using ShadcnBlazor.Spinners
|
@using ShadcnBlazor.Spinners
|
||||||
|
|
||||||
@inject AlertDialogService AlertService
|
@inject AlertDialogService AlertService
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Updating...
|
Updating instance...
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</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">
|
||||||
@for (var i = 0; i < Steps.Length; i++)
|
<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">
|
if (CurrentStep == i)
|
||||||
<Spinner ClassName="size-4" />
|
|
||||||
<span>
|
|
||||||
@Steps[i]
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (i < CurrentStep)
|
|
||||||
{
|
{
|
||||||
<div class="flex flex-row items-center gap-x-2">
|
<div class="flex flex-row items-center gap-x-2">
|
||||||
<CheckIcon ClassName="text-green-500 size-4" />
|
<Spinner ClassName="size-4"/>
|
||||||
<span>
|
<span>
|
||||||
@Steps[i]
|
@Steps[i]
|
||||||
</span>
|
</span>
|
||||||
@@ -39,13 +33,33 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="text-muted-foreground flex flex-row items-center gap-x-2">
|
if (i < CurrentStep)
|
||||||
<span class="size-4"></span>
|
{
|
||||||
@Steps[i]
|
<div class="flex flex-row items-center gap-x-2">
|
||||||
</div>
|
<CheckIcon ClassName="text-green-500 size-4"/>
|
||||||
|
<span>
|
||||||
|
@Steps[i]
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-muted-foreground flex flex-row items-center gap-x-2">
|
||||||
|
<span class="size-4"></span>
|
||||||
|
@Steps[i]
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
</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>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -62,12 +76,15 @@
|
|||||||
[
|
[
|
||||||
"Preparing",
|
"Preparing",
|
||||||
"Updating configuration files",
|
"Updating configuration files",
|
||||||
|
"Starting rebuild task",
|
||||||
"Building docker image",
|
"Building docker image",
|
||||||
"Redeploying container instance",
|
"Redeploying container instance",
|
||||||
"Waiting for container instance to start up",
|
"Waiting for container instance to start up",
|
||||||
"Update complete"
|
"Update complete"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private List<string?> LogLines = new();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (!firstRender)
|
if (!firstRender)
|
||||||
@@ -83,30 +100,100 @@
|
|||||||
Progress = 20;
|
Progress = 20;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
await Task.Delay(6000);
|
|
||||||
|
|
||||||
CurrentStep = 2;
|
|
||||||
Progress = 40;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
|
|
||||||
CurrentStep = 3;
|
CurrentStep = 2;
|
||||||
Progress = 60;
|
Progress = 30;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
await Task.Delay(4000);
|
var response = await HttpClient.SendAsync(
|
||||||
|
new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild"),
|
||||||
|
HttpCompletionOption.ResponseHeadersRead
|
||||||
|
);
|
||||||
|
|
||||||
CurrentStep = 4;
|
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||||
Progress = 80;
|
using var streamReader = new StreamReader(responseStream);
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
await Task.Delay(4000);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// TODO: Log
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (true);
|
||||||
|
|
||||||
CurrentStep = 5;
|
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;
|
Progress = 100;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
|
|
||||||
await AlertService.SuccessAsync(
|
await AlertService.SuccessAsync(
|
||||||
|
|||||||
@@ -160,5 +160,6 @@
|
|||||||
private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync<UpdateInstanceModal>(onConfigure: model =>
|
private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync<UpdateInstanceModal>(onConfigure: model =>
|
||||||
{
|
{
|
||||||
model.ShowCloseButton = false;
|
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 System.Text.Json.Serialization;
|
||||||
|
using Moonlight.Shared.Http.Events;
|
||||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||||
using Moonlight.Shared.Http.Requests.Roles;
|
using Moonlight.Shared.Http.Requests.Roles;
|
||||||
using Moonlight.Shared.Http.Requests.Themes;
|
using Moonlight.Shared.Http.Requests.Themes;
|
||||||
@@ -43,6 +44,9 @@ namespace Moonlight.Shared.Http;
|
|||||||
[JsonSerializable(typeof(UpdateThemeDto))]
|
[JsonSerializable(typeof(UpdateThemeDto))]
|
||||||
[JsonSerializable(typeof(PagedData<ThemeDto>))]
|
[JsonSerializable(typeof(PagedData<ThemeDto>))]
|
||||||
[JsonSerializable(typeof(ThemeDto))]
|
[JsonSerializable(typeof(ThemeDto))]
|
||||||
|
|
||||||
|
// Events
|
||||||
|
[JsonSerializable(typeof(RebuildEvent))]
|
||||||
public partial class SerializationContext : JsonSerializerContext
|
public partial class SerializationContext : JsonSerializerContext
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
30
compose.yaml
30
compose.yaml
@@ -36,4 +36,32 @@
|
|||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
- "Logging__LogLevel__Default=Information"
|
- "Logging__LogLevel__Default=Information"
|
||||||
- "Logging__LogLevel__Microsoft.AspNetCore=Warning"
|
- "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