Started implementing server installation

This commit is contained in:
2025-02-13 21:23:35 +01:00
parent f45699f300
commit 761ab455f0
11 changed files with 179 additions and 19 deletions

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions; using MoonCore.Extended.Abstractions;
using MoonCore.Models; using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities;
@@ -96,4 +97,23 @@ public class RemoteServersController : Controller
TotalPages = total == 0 ? 0 : total / pageSize TotalPages = total == 0 ? 0 : total / pageSize
}; };
} }
[HttpGet("{id:int}/install")]
public async Task<ServerInstallDataResponse> 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
};
}
} }

View File

@@ -380,6 +380,7 @@ public class StarImportExportService
// Fix up special variables // Fix up special variables
entry.Value = entry.Value.Replace("server.allocations.default.port", "SERVER_PORT"); 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); pc.Entries.Add(entry);
} }

View File

@@ -30,6 +30,8 @@ public partial class Server
await LogToConsole("Removing container"); await LogToConsole("Removing container");
await dockerClient.Containers.RemoveContainerAsync(container.ID, new()); await dockerClient.Containers.RemoveContainerAsync(container.ID, new());
RuntimeContainerId = null;
} }
catch (DockerContainerNotFoundException){} catch (DockerContainerNotFoundException){}

View File

@@ -53,7 +53,8 @@ public partial class Server
.OnEntryAsync(InternalStop); .OnEntryAsync(InternalStop);
StateMachine.Configure(ServerState.Installing) StateMachine.Configure(ServerState.Installing)
.Permit(ServerTrigger.NotifyInstallContainerDied, ServerState.Offline); .Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline)
.OnEntryAsync(InternalInstall);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -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<RemoteService>();
using var remoteHttpClient = await remoteService.CreateHttpClient();
var installData = await remoteHttpClient.GetJson<ServerInstallDataResponse>($"api/servers/remote/servers/{Configuration.Id}/install");
var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>();
// 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<DockerClient>();
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
InstallationContainerId = container.ID;
await AttachConsole(InstallationContainerId);
await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new());
}
}

View File

@@ -35,11 +35,11 @@ public partial class Server
var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>(); var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>();
var hostPath = PathBuilder.Dir( var hostPath = PathBuilder.Dir(
appConfiguration.Storage.Volumes, appConfiguration.Storage.Install,
Configuration.Id.ToString() Configuration.Id.ToString()
); );
await LogToConsole("Creating storage"); await LogToConsole("Creating installation storage");
// Create volume if missing // Create volume if missing
if (!Directory.Exists(hostPath)) if (!Directory.Exists(hostPath))

View File

@@ -8,6 +8,5 @@ public enum ServerTrigger
Kill = 3, Kill = 3,
Reinstall = 4, Reinstall = 4,
NotifyOnline = 5, NotifyOnline = 5,
NotifyContainerDied = 6, NotifyContainerDied = 6
NotifyInstallContainerDied = 7
} }

View File

@@ -7,7 +7,8 @@ namespace MoonlightServers.Daemon.Extensions;
public static class ServerConfigurationExtensions 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(); var parameters = configuration.ToSharedCreateParameters();
@@ -123,6 +124,63 @@ public static class ServerConfigurationExtensions
return parameters; 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<Mount>();
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) private static CreateContainerParameters ToSharedCreateParameters(this ServerConfiguration configuration)
{ {
var parameters = new CreateContainerParameters() var parameters = new CreateContainerParameters()
@@ -168,7 +226,8 @@ public static class ServerConfigurationExtensions
// Finalize limits by converting and updating the host config // Finalize limits by converting and updating the host config
parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes; parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes;
parameters.HostConfig.MemoryReservation = ByteConverter.FromMegaBytes(memoryLimit, 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 #endregion

View File

@@ -37,4 +37,15 @@ public class ServerPowerController : Controller
await server.Stop(); 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();
}
} }

View File

@@ -116,7 +116,7 @@ public class ServerService : IHostedLifecycleService
Server? server; Server? server;
lock (Servers) 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 // TODO: Maybe implement a lookup for containers which id isn't set in the cache

View File

@@ -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; }
}