From 761ab455f0279996696f669bdd004d939a56d6e6 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Thu, 13 Feb 2025 21:23:35 +0100 Subject: [PATCH] Started implementing server installation --- .../Remote/Servers/RemoteServersController.cs | 20 +++++ .../Services/StarImportExportService.cs | 1 + .../Abstractions/Server.Destroy.cs | 2 + .../Abstractions/Server.Initialize.cs | 3 +- .../Abstractions/Server.Installation.cs | 59 +++++++++++++ .../Abstractions/Server.Storage.cs | 4 +- .../Enums/ServerTrigger.cs | 3 +- .../ServerConfigurationExtensions.cs | 85 ++++++++++++++++--- .../Servers/ServerPowerController.cs | 11 +++ .../Services/ServerService.cs | 2 +- .../Responses/ServerInstallDataResponse.cs | 8 ++ 11 files changed, 179 insertions(+), 19 deletions(-) create mode 100644 MoonlightServers.Daemon/Abstractions/Server.Installation.cs create mode 100644 MoonlightServers.DaemonShared/PanelSide/Http/Responses/ServerInstallDataResponse.cs diff --git a/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs index 38ee362..21dd5ab 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Models; using MoonlightServers.ApiServer.Database.Entities; @@ -96,4 +97,23 @@ public class RemoteServersController : Controller TotalPages = total == 0 ? 0 : total / pageSize }; } + + [HttpGet("{id:int}/install")] + public async Task GetInstall([FromRoute] int id) + { + var server = await ServerRepository + .Get() + .Include(x => x.Star) + .FirstOrDefaultAsync(x => x.Id == id); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + return new ServerInstallDataResponse() + { + Script = server.Star.InstallScript, + DockerImage = server.Star.InstallDockerImage, + Shell = server.Star.InstallShell + }; + } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Services/StarImportExportService.cs b/MoonlightServers.ApiServer/Services/StarImportExportService.cs index c8b375b..d28d7e6 100644 --- a/MoonlightServers.ApiServer/Services/StarImportExportService.cs +++ b/MoonlightServers.ApiServer/Services/StarImportExportService.cs @@ -380,6 +380,7 @@ public class StarImportExportService // Fix up special variables entry.Value = entry.Value.Replace("server.allocations.default.port", "SERVER_PORT"); + entry.Value = entry.Value.Replace("server.build.default.port", "SERVER_PORT"); pc.Entries.Add(entry); } diff --git a/MoonlightServers.Daemon/Abstractions/Server.Destroy.cs b/MoonlightServers.Daemon/Abstractions/Server.Destroy.cs index a0b1c53..8378408 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Destroy.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Destroy.cs @@ -30,6 +30,8 @@ public partial class Server await LogToConsole("Removing container"); await dockerClient.Containers.RemoveContainerAsync(container.ID, new()); + + RuntimeContainerId = null; } catch (DockerContainerNotFoundException){} diff --git a/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs b/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs index 05dab76..b0709ec 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs @@ -53,7 +53,8 @@ public partial class Server .OnEntryAsync(InternalStop); StateMachine.Configure(ServerState.Installing) - .Permit(ServerTrigger.NotifyInstallContainerDied, ServerState.Offline); + .Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline) + .OnEntryAsync(InternalInstall); return Task.CompletedTask; } diff --git a/MoonlightServers.Daemon/Abstractions/Server.Installation.cs b/MoonlightServers.Daemon/Abstractions/Server.Installation.cs new file mode 100644 index 0000000..4c5fb75 --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.Installation.cs @@ -0,0 +1,59 @@ +using Docker.DotNet; +using MoonCore.Helpers; +using MoonlightServers.Daemon.Enums; +using MoonlightServers.Daemon.Extensions; +using MoonlightServers.Daemon.Services; +using MoonlightServers.DaemonShared.PanelSide.Http.Responses; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + public async Task Install() => await StateMachine.FireAsync(ServerTrigger.Reinstall); + + private async Task InternalInstall() + { + // TODO: Consider if checking for existing install containers is actually useful, because + // when the daemon is starting and a installation is still ongoing it will reattach anyways + // and the container has the auto remove flag enabled by default (maybe also consider this for the normal runtime container) + + await LogToConsole("Fetching installation configuration"); + + // Fetching remote configuration + var remoteService = ServiceProvider.GetRequiredService(); + using var remoteHttpClient = await remoteService.CreateHttpClient(); + + var installData = await remoteHttpClient.GetJson($"api/servers/remote/servers/{Configuration.Id}/install"); + + var dockerImageService = ServiceProvider.GetRequiredService(); + + // We call an external service for that, as we want to have a central management point of images + // for analytics and automatic deletion + await dockerImageService.Ensure(installData.DockerImage, async message => { await LogToConsole(message); }); + + // Ensuring storage configuration + var installationHostPath = await EnsureInstallationVolume(); + var runtimeHostPath = await EnsureRuntimeVolume(); + + // Write installation script to path + await File.WriteAllTextAsync(PathBuilder.File(installationHostPath, "install.sh"), installData.Script.Replace("\n\r", "\n") + "\n\n"); + + // Creating container configuration + var parameters = Configuration.ToInstallationCreateParameters( + runtimeHostPath, + installationHostPath, + InstallationContainerName, + installData.DockerImage, + installData.Shell + ); + + var dockerClient = ServiceProvider.GetRequiredService(); + + var container = await dockerClient.Containers.CreateContainerAsync(parameters); + InstallationContainerId = container.ID; + + await AttachConsole(InstallationContainerId); + + await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new()); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Storage.cs b/MoonlightServers.Daemon/Abstractions/Server.Storage.cs index f562706..4acc8d2 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Storage.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Storage.cs @@ -35,11 +35,11 @@ public partial class Server var appConfiguration = ServiceProvider.GetRequiredService(); var hostPath = PathBuilder.Dir( - appConfiguration.Storage.Volumes, + appConfiguration.Storage.Install, Configuration.Id.ToString() ); - await LogToConsole("Creating storage"); + await LogToConsole("Creating installation storage"); // Create volume if missing if (!Directory.Exists(hostPath)) diff --git a/MoonlightServers.Daemon/Enums/ServerTrigger.cs b/MoonlightServers.Daemon/Enums/ServerTrigger.cs index 9668299..014675f 100644 --- a/MoonlightServers.Daemon/Enums/ServerTrigger.cs +++ b/MoonlightServers.Daemon/Enums/ServerTrigger.cs @@ -8,6 +8,5 @@ public enum ServerTrigger Kill = 3, Reinstall = 4, NotifyOnline = 5, - NotifyContainerDied = 6, - NotifyInstallContainerDied = 7 + NotifyContainerDied = 6 } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerConfigurationExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerConfigurationExtensions.cs index 1dfda0f..8199156 100644 --- a/MoonlightServers.Daemon/Extensions/ServerConfigurationExtensions.cs +++ b/MoonlightServers.Daemon/Extensions/ServerConfigurationExtensions.cs @@ -7,10 +7,11 @@ namespace MoonlightServers.Daemon.Extensions; public static class ServerConfigurationExtensions { - public static CreateContainerParameters ToRuntimeCreateParameters(this ServerConfiguration configuration, string hostPath, string containerName) + public static CreateContainerParameters ToRuntimeCreateParameters(this ServerConfiguration configuration, + string hostPath, string containerName) { var parameters = configuration.ToSharedCreateParameters(); - + #region Security parameters.HostConfig.CapDrop = new List() @@ -75,7 +76,7 @@ public static class ServerConfigurationExtensions #region Mounts parameters.HostConfig.Mounts = new List(); - + parameters.HostConfig.Mounts.Add(new() { Source = hostPath, @@ -85,7 +86,7 @@ public static class ServerConfigurationExtensions }); #endregion - + #region Port Bindings if (true) // TODO: Add network toggle @@ -106,7 +107,7 @@ public static class ServerConfigurationExtensions HostIP = allocation.IpAddress } }); - + parameters.HostConfig.PortBindings.Add($"{allocation.Port}/udp", new List { new() @@ -123,6 +124,63 @@ public static class ServerConfigurationExtensions return parameters; } + public static CreateContainerParameters ToInstallationCreateParameters( + this ServerConfiguration configuration, + string runtimeHostPath, + string installationHostPath, + string containerName, + string installDockerImage, + string installShell + ) + { + var parameters = configuration.ToSharedCreateParameters(); + + // - Name + parameters.Name = containerName; + parameters.Hostname = containerName; + + // - Image + parameters.Image = installDockerImage; + + // - Env + parameters.Env = configuration + .ToEnvironmentVariables() + .Select(x => $"{x.Key}={x.Value}") + .ToList(); + + // -- Working directory + parameters.WorkingDir = "/mnt/server"; + + // - User + // Note: Some images might not work if we set a user here + parameters.User = "0:0"; + + // -- Mounts + parameters.HostConfig.Mounts = new List(); + + parameters.HostConfig.Mounts.Add(new() + { + Source = runtimeHostPath, + Target = "/mnt/server", + ReadOnly = false, + Type = "bind" + }); + + parameters.HostConfig.Mounts.Add(new() + { + Source = installationHostPath, + Target = "/mnt/install", + ReadOnly = false, + Type = "bind" + }); + + parameters.Cmd = [installShell, "/mnt/install/install.sh"]; + + parameters.HostConfig.AutoRemove = true; + + return parameters; + } + private static CreateContainerParameters ToSharedCreateParameters(this ServerConfiguration configuration) { var parameters = new CreateContainerParameters() @@ -158,7 +216,7 @@ public static class ServerConfigurationExtensions long swapLimit = -1; /* - + // If swap is enabled globally and not disabled on this server, set swap if (!configuration.Limits.DisableSwap && config.Server.EnableSwap) swapLimit = (long)(memoryOverhead + memoryOverhead * config.Server.SwapMultiplier); @@ -168,7 +226,8 @@ public static class ServerConfigurationExtensions // Finalize limits by converting and updating the host config parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes; parameters.HostConfig.MemoryReservation = ByteConverter.FromMegaBytes(memoryLimit, 1000).Bytes; - parameters.HostConfig.MemorySwap = swapLimit == -1 ? swapLimit : ByteConverter.FromMegaBytes(swapLimit, 1000).Bytes; + parameters.HostConfig.MemorySwap = + swapLimit == -1 ? swapLimit : ByteConverter.FromMegaBytes(swapLimit, 1000).Bytes; #endregion @@ -184,7 +243,7 @@ public static class ServerConfigurationExtensions #region DNS // TODO: Read hosts dns settings? - + parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List() { "1.1.1.1", @@ -213,9 +272,9 @@ public static class ServerConfigurationExtensions #endregion #region Labels - + parameters.Labels = new Dictionary(); - + parameters.Labels.Add("Software", "Moonlight-Panel"); parameters.Labels.Add("ServerId", configuration.Id.ToString()); @@ -223,7 +282,7 @@ public static class ServerConfigurationExtensions return parameters; } - + public static Dictionary ToEnvironmentVariables(this ServerConfiguration configuration) { var result = new Dictionary @@ -236,11 +295,11 @@ public static class ServerConfigurationExtensions if (configuration.Allocations.Length > 0) { var mainAllocation = configuration.Allocations.First(); - + result.Add("SERVER_IP", mainAllocation.IpAddress); result.Add("SERVER_PORT", mainAllocation.Port.ToString()); } - + // Handle allocation variables var i = 1; foreach (var allocation in configuration.Allocations) diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs index 014b08f..bf16335 100644 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs @@ -37,4 +37,15 @@ public class ServerPowerController : Controller await server.Stop(); } + + [HttpPost("{serverId:int}/install")] + public async Task Install(int serverId, [FromQuery] bool runAsync = true) + { + var server = ServerService.GetServer(serverId); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + await server.Install(); + } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs index 73e59a6..28d4bfb 100644 --- a/MoonlightServers.Daemon/Services/ServerService.cs +++ b/MoonlightServers.Daemon/Services/ServerService.cs @@ -116,7 +116,7 @@ public class ServerService : IHostedLifecycleService Server? server; lock (Servers) - server = Servers.FirstOrDefault(x => x.RuntimeContainerId == message.ID); + server = Servers.FirstOrDefault(x => x.RuntimeContainerId == message.ID || x.InstallationContainerId == message.ID); // TODO: Maybe implement a lookup for containers which id isn't set in the cache diff --git a/MoonlightServers.DaemonShared/PanelSide/Http/Responses/ServerInstallDataResponse.cs b/MoonlightServers.DaemonShared/PanelSide/Http/Responses/ServerInstallDataResponse.cs new file mode 100644 index 0000000..2938ab9 --- /dev/null +++ b/MoonlightServers.DaemonShared/PanelSide/Http/Responses/ServerInstallDataResponse.cs @@ -0,0 +1,8 @@ +namespace MoonlightServers.DaemonShared.PanelSide.Http.Responses; + +public class ServerInstallDataResponse +{ + public string Shell { get; set; } + public string DockerImage { get; set; } + public string Script { get; set; } +} \ No newline at end of file