Refactored container helper service. Cleaned up event models. Implemented version changing. Added security questions before rebuild
This commit is contained in:
@@ -2,19 +2,21 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Api.Services;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/ch")]
|
||||
public class ChController : Controller
|
||||
public class ContainerHelperController : Controller
|
||||
{
|
||||
private readonly ContainerHelperService ContainerHelperService;
|
||||
private readonly IOptions<ContainerHelperOptions> Options;
|
||||
|
||||
public ChController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
|
||||
public ContainerHelperController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
|
||||
{
|
||||
ContainerHelperService = containerHelperService;
|
||||
Options = options;
|
||||
@@ -35,9 +37,17 @@ public class ChController : Controller
|
||||
public Task<IResult> RebuildAsync()
|
||||
{
|
||||
var result = ContainerHelperService.RebuildAsync();
|
||||
var mappedResult = result.Select(ContainerHelperMapper.ToDto);
|
||||
|
||||
return Task.FromResult<IResult>(
|
||||
TypedResults.ServerSentEvents(result)
|
||||
TypedResults.ServerSentEvents(mappedResult)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("version")]
|
||||
public async Task<ActionResult> SetVersionAsync([FromBody] SetVersionDto request)
|
||||
{
|
||||
await ContainerHelperService.SetVersionAsync(request.Version);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||
|
||||
public struct RebuildEventDto
|
||||
{
|
||||
[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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||
|
||||
public class ProblemDetails
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int Status { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
public Dictionary<string, string[]>? Errors { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
|
||||
public record SetVersionDto(string Version);
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||
|
||||
[JsonSerializable(typeof(SetVersionDto))]
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
[JsonSerializable(typeof(RebuildEventDto))]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
private static JsonSerializerOptions? InternalTunedOptions;
|
||||
|
||||
public static JsonSerializerOptions TunedOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (InternalTunedOptions != null)
|
||||
return InternalTunedOptions;
|
||||
|
||||
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
|
||||
|
||||
return InternalTunedOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Moonlight.Api/Mappers/ContainerHelperMapper.cs
Normal file
13
Moonlight.Api/Mappers/ContainerHelperMapper.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Events;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
[Mapper]
|
||||
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||
public static partial class ContainerHelperMapper
|
||||
{
|
||||
public static partial RebuildEventDto ToDto(Http.Services.ContainerHelper.Events.RebuildEventDto rebuildEventDto);
|
||||
}
|
||||
@@ -35,4 +35,8 @@
|
||||
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Http\Services\ContainerHelper\Responses\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using Moonlight.Shared.Http;
|
||||
using Moonlight.Shared.Http.Events;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
@@ -30,61 +32,73 @@ public class ContainerHelperService
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<RebuildEvent> RebuildAsync()
|
||||
public async IAsyncEnumerable<RebuildEventDto> 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()
|
||||
|
||||
yield return new RebuildEventDto()
|
||||
{
|
||||
Type = RebuildEventType.Failed,
|
||||
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)
|
||||
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
|
||||
var data = line.Trim("data: ");
|
||||
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEvent>(data, options);
|
||||
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.TunedOptions);
|
||||
|
||||
yield return deserializedData;
|
||||
|
||||
|
||||
// Exit if service will go down for a clean exit
|
||||
if(deserializedData is {Type: RebuildEventType.Step, Data: "ServiceDown"})
|
||||
if (deserializedData is { Type: RebuildEventType.Step, Data: "ServiceDown" })
|
||||
yield break;
|
||||
|
||||
} while (true);
|
||||
|
||||
yield return new RebuildEvent()
|
||||
|
||||
yield return new RebuildEventDto()
|
||||
{
|
||||
Type = RebuildEventType.Succeeded,
|
||||
Data = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SetVersionAsync(string version)
|
||||
{
|
||||
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"api/configuration/version",
|
||||
new SetVersionDto(version),
|
||||
SerializationContext.TunedOptions
|
||||
);
|
||||
|
||||
if(response.IsSuccessStatusCode)
|
||||
return;
|
||||
|
||||
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.TunedOptions);
|
||||
|
||||
if(problemDetails == null)
|
||||
throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}");
|
||||
|
||||
throw new HttpRequestException($"Failed to set version: {problemDetails.Detail ?? problemDetails.Title}");
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public partial class Startup
|
||||
{
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SharedSerializationContext.Default);
|
||||
});
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
|
||||
@@ -18,7 +18,7 @@ public static class Constants
|
||||
};
|
||||
|
||||
// Add source generated options from shared project
|
||||
InternalOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
InternalOptions.TypeInfoResolverChain.Add(SharedSerializationContext.Default);
|
||||
|
||||
return InternalOptions;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using System.Text.Json
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Events
|
||||
@using Moonlight.Shared.Http.Requests.Admin.ContainerHelper
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Progresses
|
||||
@@ -110,7 +111,10 @@ else
|
||||
Progress = 20;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(2000);
|
||||
await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
|
||||
{
|
||||
Version = Version
|
||||
});
|
||||
|
||||
// Starting rebuild task
|
||||
CurrentStep = 2;
|
||||
@@ -138,7 +142,7 @@ else
|
||||
continue;
|
||||
|
||||
var data = line.Trim("data: ");
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEvent>(data, Constants.SerializerOptions);
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, Constants.SerializerOptions);
|
||||
|
||||
switch (deserializedData.Type)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@using System.Text.RegularExpressions
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@@ -39,14 +40,15 @@
|
||||
</SelectTrigger>
|
||||
<SelectContent ClassName="w-64">
|
||||
<SelectItem Value="v2.1">v2.1</SelectItem>
|
||||
<SelectItem Value="v2.1.1">v2.1.1</SelectItem>
|
||||
<SelectItem Value="feat/ContainerHelper">feat/ContainerHelper
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal">
|
||||
<Button @onclick="ApplyAsync">Apply</Button>
|
||||
<Button @onclick="AskApplyAsync">Apply</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
@@ -116,20 +118,73 @@
|
||||
|
||||
private async Task ApplyAsync()
|
||||
{
|
||||
await AlertDialogService.ConfirmDangerAsync(
|
||||
"Moonlight Rebuild",
|
||||
"If you continue the moonlight instance will become unavailable during the rebuild process. This will impact users on this instance",
|
||||
async () =>
|
||||
await DialogService.LaunchAsync<UpdateInstanceModal>(
|
||||
parameters => { parameters[nameof(UpdateInstanceModal.Version)] = SelectedVersion; },
|
||||
onConfigure: model =>
|
||||
{
|
||||
await DialogService.LaunchAsync<UpdateInstanceModal>(
|
||||
parameters => { parameters[nameof(UpdateInstanceModal.Version)] = SelectedVersion; },
|
||||
onConfigure: model =>
|
||||
{
|
||||
model.ShowCloseButton = false;
|
||||
model.ClassName = "sm:max-w-4xl!";
|
||||
}
|
||||
);
|
||||
model.ShowCloseButton = false;
|
||||
model.ClassName = "sm:max-w-4xl!";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async Task AskApplyAsync()
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(SelectedVersion))
|
||||
return;
|
||||
|
||||
var shouldContinue = await ConfirmRiskyVersionAsync(
|
||||
"Moonlight Rebuild",
|
||||
"If you continue the moonlight instance will become unavailable during the rebuild process. This will impact users on this instance"
|
||||
);
|
||||
|
||||
if(!shouldContinue)
|
||||
return;
|
||||
|
||||
if (!Regex.IsMatch(SelectedVersion, @"^v\d+(\.\d+)*b?$"))
|
||||
{
|
||||
shouldContinue = await ConfirmRiskyVersionAsync(
|
||||
"Development Version",
|
||||
"You are about to install development a version. This can break your instance. Continue at your own risk"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SelectedVersion.EndsWith('b'))
|
||||
{
|
||||
shouldContinue = await ConfirmRiskyVersionAsync(
|
||||
"Beta / Pre-Release Version",
|
||||
"You are about to install a version marked as pre-release / beta. This can break your instance. Continue at your own risk"
|
||||
);
|
||||
}
|
||||
else
|
||||
shouldContinue = true;
|
||||
}
|
||||
|
||||
if (!shouldContinue)
|
||||
return;
|
||||
|
||||
await ApplyAsync();
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmRiskyVersionAsync(string title, string message)
|
||||
{
|
||||
var tcs = new TaskCompletionSource();
|
||||
var confirmed = false;
|
||||
|
||||
await AlertDialogService.ConfirmDangerAsync(
|
||||
title,
|
||||
message,
|
||||
() =>
|
||||
{
|
||||
confirmed = true;
|
||||
tcs.SetResult();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
);
|
||||
|
||||
await tcs.Task;
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Moonlight.Shared.Http.Events;
|
||||
|
||||
public struct RebuildEvent
|
||||
public struct RebuildEventDto
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public RebuildEventType Type { get; set; }
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||
|
||||
public class SetVersionDto
|
||||
{
|
||||
[Required]
|
||||
[RegularExpression(@"^(?!\/|.*\/\/|.*\.\.|.*\/$)[A-Za-z0-9._/-]+$", ErrorMessage = "Invalid version format")]
|
||||
public string Version { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Shared.Http.Events;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
@@ -46,13 +47,28 @@ namespace Moonlight.Shared.Http;
|
||||
[JsonSerializable(typeof(ThemeDto))]
|
||||
|
||||
// Events
|
||||
[JsonSerializable(typeof(RebuildEvent))]
|
||||
[JsonSerializable(typeof(RebuildEventDto))]
|
||||
|
||||
// Container Helper
|
||||
[JsonSerializable(typeof(ContainerHelperStatusDto))]
|
||||
|
||||
// Misc
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
public partial class SharedSerializationContext : JsonSerializerContext
|
||||
{
|
||||
private static JsonSerializerOptions? InternalTunedOptions;
|
||||
|
||||
public static JsonSerializerOptions TunedOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (InternalTunedOptions != null)
|
||||
return InternalTunedOptions;
|
||||
|
||||
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
|
||||
|
||||
return InternalTunedOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user