Started with adding container creation and a server state machine

This commit is contained in:
2024-12-26 21:09:22 +01:00
parent 9f8c1f6d24
commit 039db22207
12 changed files with 548 additions and 32 deletions

View File

@@ -0,0 +1,118 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.Helpers;
public class ServerActionHelper
{
public static async Task Start(Server server, IServiceProvider serviceProvider)
{
await EnsureStorage(server, serviceProvider);
await EnsureDockerImage(server, serviceProvider);
await CreateRuntimeContainer(server, serviceProvider);
await StartRuntimeContainer(server, serviceProvider);
}
private static async Task EnsureStorage(Server server, IServiceProvider serviceProvider)
{
await NotifyTask(server, serviceProvider, ServerTask.CreatingStorage);
// Build paths
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
var volumePath = PathBuilder.Dir(
configuration.Storage.Volumes,
server.Configuration.Id.ToString()
);
// Create volume if missing
if (!Directory.Exists(volumePath))
Directory.CreateDirectory(volumePath);
// TODO: Virtual disk
}
private static async Task EnsureDockerImage(Server server, IServiceProvider serviceProvider)
{
await NotifyTask(server, serviceProvider, ServerTask.PullingDockerImage);
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
await dockerClient.Images.CreateImageAsync(new()
{
FromImage = server.Configuration.DockerImage
},
new AuthConfig(),
new Progress<JSONMessage>(async message =>
{
//var percentage = (int)(message.Progress.Current / message.Progress.Total);
//await UpdateProgress(server, serviceProvider, percentage);
})
);
}
private static async Task CreateRuntimeContainer(Server server, IServiceProvider serviceProvider)
{
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
try
{
var existingContainer = await dockerClient.Containers.InspectContainerAsync(
$"moonlight-runtime-{server.Configuration.Id}"
);
await NotifyTask(server, serviceProvider, ServerTask.RemovingContainer);
if (existingContainer.State.Running) // Stop already running container
{
await dockerClient.Containers.StopContainerAsync(existingContainer.ID, new()
{
WaitBeforeKillSeconds = 30 // TODO: Config
});
}
await dockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
}
catch (DockerContainerNotFoundException)
{
}
await NotifyTask(server, serviceProvider, ServerTask.CreatingContainer);
// Create a new container
var parameters = new CreateContainerParameters();
ServerConfigurationHelper.ApplyRuntimeOptions(
parameters,
server.Configuration,
serviceProvider.GetRequiredService<AppConfiguration>()
);
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
server.ContainerId = container.ID;
}
private static async Task StartRuntimeContainer(Server server, IServiceProvider serviceProvider)
{
await NotifyTask(server, serviceProvider, ServerTask.StartingContainer);
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
await dockerClient.Containers.StartContainerAsync(server.ContainerId, new());
}
private static async Task NotifyTask(Server server, IServiceProvider serviceProvider, ServerTask task)
{
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger($"Server {server.Configuration.Id}");
logger.LogInformation("Task: {task}", task);
}
private static async Task UpdateProgress(Server server, IServiceProvider serviceProvider, int progress)
{
}
}

View File

@@ -0,0 +1,202 @@
using Docker.DotNet.Models;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models.Cache;
namespace MoonlightServers.Daemon.Helpers;
public static class ServerConfigurationHelper
{
public static void ApplyRuntimeOptions(CreateContainerParameters parameters, ServerConfiguration configuration, AppConfiguration appConfiguration)
{
ApplySharedOptions(parameters, configuration);
// -- Cap drops
parameters.HostConfig.CapDrop = new List<string>()
{
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap"
};
// -- More security options
parameters.HostConfig.ReadonlyRootfs = true;
parameters.HostConfig.SecurityOpt = new List<string>()
{
"no-new-privileges"
};
// - Name
var name = $"moonlight-runtime-{configuration.Id}";
parameters.Name = name;
parameters.Hostname = name;
// - Image
parameters.Image = configuration.DockerImage;
// - Env
parameters.Env = ConstructEnv(configuration)
.Select(x => $"{x.Key}={x.Value}")
.ToList();
// -- Working directory
parameters.WorkingDir = "/home/container";
// - User
//TODO: use config
parameters.User = $"998:998";
// -- Mounts
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new()
{
Source = GetRuntimeVolume(configuration, appConfiguration),
Target = "/home/container",
ReadOnly = false,
Type = "bind"
});
// -- Ports
//var config = configService.Get();
if (true) // TODO: Add network toggle
{
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
foreach (var allocation in configuration.Allocations)
{
parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new());
parameters.ExposedPorts.Add($"{allocation.Port}/udp", new());
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/tcp", new List<PortBinding>
{
new()
{
HostPort = allocation.Port.ToString(),
HostIP = allocation.IpAddress
}
});
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/udp", new List<PortBinding>
{
new()
{
HostPort = allocation.Port.ToString(),
HostIP = allocation.IpAddress
}
});
}
}
}
public static void ApplySharedOptions(CreateContainerParameters parameters, ServerConfiguration configuration)
{
// - Input, output & error streams and tty
parameters.Tty = true;
parameters.AttachStderr = true;
parameters.AttachStdin = true;
parameters.AttachStdout = true;
parameters.OpenStdin = true;
// - Host config
parameters.HostConfig = new HostConfig();
// -- CPU limits
parameters.HostConfig.CPUQuota = configuration.Cpu * 1000;
parameters.HostConfig.CPUPeriod = 100000;
parameters.HostConfig.CPUShares = 1024;
// -- Memory and swap limits
var memoryLimit = configuration.Memory;
// The overhead multiplier gives the container a little bit more memory to prevent crashes
var memoryOverhead = memoryLimit + (memoryLimit * 0.05f); // TODO: Config
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);
co
*/
// 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;
// -- Other limits
parameters.HostConfig.BlkioWeight = 100;
//container.HostConfig.PidsLimit = configuration.Limits.PidsLimit;
parameters.HostConfig.OomKillDisable = true; //!configuration.Limits.EnableOomKill;
// -- DNS
parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List<string>()
{
"1.1.1.1",
"9.9.9.9"
};
// -- Tmpfs
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
{
{ "/tmp", $"rw,exec,nosuid,size=100M" } // TODO: Config
};
// -- Logging
parameters.HostConfig.LogConfig = new()
{
Type = "json-file", // We need to use this provider, as the GetLogs endpoint needs it
Config = new Dictionary<string, string>()
};
// - Labels
parameters.Labels = new Dictionary<string, string>();
parameters.Labels.Add("Software", "Moonlight-Panel");
parameters.Labels.Add("ServerId", configuration.Id.ToString());
}
public static Dictionary<string, string> ConstructEnv(ServerConfiguration configuration)
{
var result = new Dictionary<string, string>();
// Default environment variables
//TODO: Add timezone, add server ip
result.Add("STARTUP", configuration.StartupCommand);
result.Add("SERVER_MEMORY", configuration.Memory.ToString());
if (configuration.Allocations.Length > 0)
{
var mainAllocation = configuration.Allocations.First();
result.Add("SERVER_IP", mainAllocation.IpAddress);
result.Add("SERVER_PORT", mainAllocation.Port.ToString());
}
// Handle additional allocation variables
var i = 1;
foreach (var additionalAllocation in configuration.Allocations)
{
result.Add($"ML_PORT_{i}", additionalAllocation.Port.ToString());
i++;
}
// Copy variables as env vars
foreach (var variable in configuration.Variables)
result.Add(variable.Key, variable.Value);
return result;
}
public static string GetRuntimeVolume(ServerConfiguration configuration, AppConfiguration appConfiguration)
{
var localPath = PathBuilder.Dir(appConfiguration.Storage.Volumes, configuration.Id.ToString());
var absolutePath = Path.GetFullPath(localPath);
return absolutePath;
}
}

View File

@@ -0,0 +1,69 @@
namespace MoonlightServers.Daemon.Helpers;
public class StateMachine<T> where T : struct, Enum
{
private readonly List<StateMachineTransition> Transitions = new();
private readonly object Lock = new();
public T CurrentState { get; private set; }
public event Func<T, Task> OnTransitioned;
public event Action<T, Exception> OnError;
public StateMachine(T initialState)
{
CurrentState = initialState;
}
public void AddTransition(T from, T to, T? onError, Func<Task> fun)
{
Transitions.Add(new()
{
From = from,
To = to,
OnError = onError,
OnTransitioning = fun
});
}
public void AddTransition(T from, T to, Func<Task> fun) => AddTransition(from, to, null, fun);
public async Task TransitionTo(T to)
{
lock (Lock)
{
var transition = Transitions.FirstOrDefault(x =>
x.From.Equals(CurrentState) &&
x.To.Equals(to)
);
if (transition == null)
throw new InvalidOperationException("Unable to transition to the request state: No transition found");
try
{
transition.OnTransitioning.Invoke().Wait();
}
catch (Exception e)
{
if(OnError != null)
OnError.Invoke(to, e);
if (transition.OnError.HasValue)
CurrentState = transition.OnError.Value;
else
throw new AggregateException("An error occured while transitioning to a state", e);
}
}
if(OnTransitioned != null)
await OnTransitioned.Invoke(CurrentState);
}
public class StateMachineTransition
{
public T From { get; set; }
public T To { get; set; }
public T? OnError { get; set; }
public Func<Task> OnTransitioning { get; set; }
}
}