Recreated plugin with new project template. Started implementing server system daemon
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
using System.ComponentModel;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonlightServers.Daemon.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public static class ConfigMapper
|
||||
{
|
||||
public static CreateContainerParameters GetRuntimeConfig(
|
||||
string uuid,
|
||||
string name,
|
||||
RuntimeConfiguration configuration,
|
||||
string runtimeStoragePath
|
||||
)
|
||||
{
|
||||
var parameters = new CreateContainerParameters()
|
||||
{
|
||||
HostConfig = new()
|
||||
};
|
||||
|
||||
ApplySharedOptions(parameters, configuration);
|
||||
|
||||
// Limits
|
||||
|
||||
if (configuration.Limits.CpuPercent.HasValue)
|
||||
{
|
||||
parameters.HostConfig.CPUQuota = configuration.Limits.CpuPercent.Value * 1000;
|
||||
parameters.HostConfig.CPUPeriod = 100000;
|
||||
parameters.HostConfig.CPUShares = 1024;
|
||||
}
|
||||
|
||||
if (configuration.Limits.MemoryMb.HasValue)
|
||||
{
|
||||
var memoryLimit = configuration.Limits.MemoryMb.Value;
|
||||
|
||||
// The overhead multiplier gives the container a little bit more memory to prevent crashes
|
||||
var memoryOverhead = memoryLimit + memoryLimit * 0.05f;
|
||||
|
||||
parameters.HostConfig.Memory = (long)memoryOverhead * 1024L * 1024L;
|
||||
parameters.HostConfig.MemoryReservation = (long)memoryLimit * 1024L * 1024L;
|
||||
|
||||
if (configuration.Limits.SwapMb.HasValue)
|
||||
{
|
||||
var rawSwap = configuration.Limits.SwapMb.Value * 1024L * 1024L;
|
||||
parameters.HostConfig.MemorySwap = rawSwap + (long)memoryOverhead;
|
||||
}
|
||||
}
|
||||
|
||||
parameters.HostConfig.BlkioWeight = 100;
|
||||
parameters.HostConfig.OomKillDisable = true;
|
||||
|
||||
// Storage
|
||||
|
||||
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
|
||||
{
|
||||
{ "/tmp", "rw,exec,nosuid,size=100M" } // TODO: Config
|
||||
};
|
||||
|
||||
parameters.WorkingDir = "/home/container";
|
||||
|
||||
parameters.HostConfig.Mounts = new List<Mount>();
|
||||
parameters.HostConfig.Mounts.Add(new Mount()
|
||||
{
|
||||
Source = runtimeStoragePath,
|
||||
Target = "/home/container",
|
||||
Type = "bind",
|
||||
ReadOnly = false
|
||||
});
|
||||
|
||||
// Labels
|
||||
parameters.Labels = new Dictionary<string, string>()
|
||||
{
|
||||
{ "dev.moonlightpanel", "true" },
|
||||
{ "dev.moonlightpanel.id", uuid }
|
||||
};
|
||||
|
||||
foreach (var label in configuration.Environment.Labels)
|
||||
parameters.Labels.Add(label.Key, label.Value);
|
||||
|
||||
// Security
|
||||
|
||||
parameters.HostConfig.CapDrop = new List<string>()
|
||||
{
|
||||
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
|
||||
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap"
|
||||
};
|
||||
|
||||
parameters.HostConfig.ReadonlyRootfs = true;
|
||||
parameters.HostConfig.SecurityOpt = new List<string>()
|
||||
{
|
||||
"no-new-privileges"
|
||||
};
|
||||
|
||||
// Name
|
||||
|
||||
parameters.Name = name;
|
||||
|
||||
// Docker Image
|
||||
parameters.Image = configuration.Template.DockerImage;
|
||||
|
||||
// Networking
|
||||
|
||||
if (configuration.Network.Ports.Length > 0 && !string.IsNullOrWhiteSpace(configuration.Network.FriendlyName))
|
||||
parameters.Hostname = configuration.Network.FriendlyName;
|
||||
|
||||
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
|
||||
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
|
||||
|
||||
foreach (var port in configuration.Network.Ports)
|
||||
{
|
||||
parameters.ExposedPorts.Add($"{port.Port}/tcp", new());
|
||||
parameters.ExposedPorts.Add($"{port.Port}/udp", new());
|
||||
|
||||
parameters.HostConfig.PortBindings.Add($"{port.Port}/tcp", new List<PortBinding>
|
||||
{
|
||||
new()
|
||||
{
|
||||
HostPort = port.Port.ToString(),
|
||||
HostIP = port.IpAddress
|
||||
}
|
||||
});
|
||||
|
||||
parameters.HostConfig.PortBindings.Add($"{port.Port}/udp", new List<PortBinding>
|
||||
{
|
||||
new()
|
||||
{
|
||||
HostPort = port.Port.ToString(),
|
||||
HostIP = port.IpAddress
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Force outgoing ip stuff
|
||||
|
||||
// User
|
||||
parameters.User = "1000:1000";
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public static CreateContainerParameters GetInstallConfig(
|
||||
string uuid,
|
||||
string name,
|
||||
RuntimeConfiguration runtimeConfiguration,
|
||||
InstallConfiguration installConfiguration,
|
||||
string runtimeStoragePath,
|
||||
string installStoragePath
|
||||
)
|
||||
{
|
||||
var parameters = new CreateContainerParameters()
|
||||
{
|
||||
HostConfig = new()
|
||||
};
|
||||
|
||||
ApplySharedOptions(parameters, runtimeConfiguration);
|
||||
|
||||
// Labels
|
||||
parameters.Labels = new Dictionary<string, string>()
|
||||
{
|
||||
{ "dev.moonlightpanel", "true" },
|
||||
{ "dev.moonlightpanel.id", uuid }
|
||||
};
|
||||
|
||||
foreach (var label in runtimeConfiguration.Environment.Labels)
|
||||
parameters.Labels.Add(label.Key, label.Value);
|
||||
|
||||
// Name
|
||||
|
||||
parameters.Name = name;
|
||||
|
||||
// Docker Image
|
||||
parameters.Image = installConfiguration.DockerImage;
|
||||
|
||||
// User
|
||||
parameters.User = "1000:1000";
|
||||
|
||||
// Storage
|
||||
parameters.WorkingDir = "/mnt/server";
|
||||
|
||||
parameters.HostConfig.Mounts = new List<Mount>();
|
||||
|
||||
parameters.HostConfig.Mounts.Add(new Mount()
|
||||
{
|
||||
Source = runtimeStoragePath,
|
||||
Target = "/mnt/server",
|
||||
ReadOnly = false,
|
||||
Type = "bind"
|
||||
});
|
||||
|
||||
parameters.HostConfig.Mounts.Add(new Mount()
|
||||
{
|
||||
Source = installStoragePath,
|
||||
Target = "/mnt/install",
|
||||
ReadOnly = false,
|
||||
Type = "bind"
|
||||
});
|
||||
|
||||
// Command
|
||||
parameters.Cmd = [installConfiguration.Shell, "/mnt/install/install.sh"];
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static void ApplySharedOptions(
|
||||
CreateContainerParameters parameters,
|
||||
RuntimeConfiguration configuration
|
||||
)
|
||||
{
|
||||
// Input, output & error streams and TTY
|
||||
|
||||
parameters.Tty = true;
|
||||
parameters.AttachStderr = true;
|
||||
parameters.AttachStdin = true;
|
||||
parameters.AttachStdout = true;
|
||||
parameters.OpenStdin = true;
|
||||
|
||||
// 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>()
|
||||
};
|
||||
|
||||
// Environment variables
|
||||
|
||||
parameters.Env = new List<string>()
|
||||
{
|
||||
$"STARTUP={configuration.Template.StartupCommand}",
|
||||
//TODO: Add timezone, add server ip
|
||||
};
|
||||
|
||||
if (configuration.Limits.MemoryMb.HasValue)
|
||||
parameters.Env.Add($"SERVER_MEMORY={configuration.Limits.MemoryMb.Value}");
|
||||
|
||||
if (configuration.Network.MainPort != null)
|
||||
{
|
||||
parameters.Env.Add($"SERVER_IP={configuration.Network.MainPort.IpAddress}");
|
||||
parameters.Env.Add($"SERVER_PORT={configuration.Network.MainPort.Port}");
|
||||
}
|
||||
|
||||
// Handle port variables
|
||||
var i = 1;
|
||||
foreach (var port in configuration.Network.Ports)
|
||||
{
|
||||
parameters.Env.Add($"ML_PORT_{i}={port.Port}");
|
||||
i++;
|
||||
}
|
||||
|
||||
// Copy variables as env vars
|
||||
foreach (var variable in configuration.Environment.Variables)
|
||||
parameters.Env.Add($"{variable.Key}={variable.Value}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable
|
||||
{
|
||||
public event Func<string, Task>? OnOutput;
|
||||
|
||||
private MultiplexedStream? Stream;
|
||||
|
||||
private readonly string ContainerId;
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ILogger Logger;
|
||||
|
||||
private readonly List<string> Cache = new(302);
|
||||
private readonly SemaphoreSlim CacheLock = new(1, 1);
|
||||
private readonly CancellationTokenSource Cts = new();
|
||||
|
||||
public DockerConsole(
|
||||
string containerId,
|
||||
DockerClient dockerClient,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
ContainerId = containerId;
|
||||
DockerClient = dockerClient;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task AttachAsync()
|
||||
{
|
||||
// Fetch initial logs
|
||||
Logger.LogTrace("Fetching pre-existing logs from container");
|
||||
|
||||
var logResponse = await DockerClient.Containers.GetContainerLogsAsync(
|
||||
ContainerId,
|
||||
new()
|
||||
{
|
||||
Follow = false,
|
||||
ShowStderr = true,
|
||||
ShowStdout = true
|
||||
}
|
||||
);
|
||||
|
||||
// Append to cache
|
||||
var logs = await logResponse.ReadOutputToEndAsync(Cts.Token);
|
||||
|
||||
await CacheLock.WaitAsync(Cts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
Cache.Add(logs.stdout);
|
||||
Cache.Add(logs.stderr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CacheLock.Release();
|
||||
}
|
||||
|
||||
// Stream new logs
|
||||
Logger.LogTrace("Starting log streaming");
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var capturedCt = Cts.Token;
|
||||
|
||||
Logger.LogTrace("Starting attach loop");
|
||||
|
||||
while (!capturedCt.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = await DockerClient.Containers.AttachContainerAsync(
|
||||
ContainerId,
|
||||
new ContainerAttachParameters()
|
||||
{
|
||||
Stderr = true,
|
||||
Stdin = true,
|
||||
Stdout = true,
|
||||
Stream = true
|
||||
},
|
||||
capturedCt
|
||||
);
|
||||
|
||||
// Make stream accessible from the outside
|
||||
Stream = stream;
|
||||
|
||||
const int bufferSize = 1024;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
|
||||
while (!capturedCt.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var readResult = await stream.ReadOutputAsync(buffer, 0, bufferSize, capturedCt);
|
||||
|
||||
if (readResult.Count > 0)
|
||||
{
|
||||
var decodedBuffer = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
|
||||
|
||||
await CacheLock.WaitAsync(capturedCt);
|
||||
|
||||
try
|
||||
{
|
||||
if (Cache.Count > 300)
|
||||
Cache.RemoveRange(0, 50);
|
||||
|
||||
Cache.Add(decodedBuffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CacheLock.Release();
|
||||
}
|
||||
|
||||
if (OnOutput != null)
|
||||
await OnOutput.Invoke(decodedBuffer);
|
||||
}
|
||||
|
||||
if (readResult.EOF)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An unhandled error occured while processing container stream");
|
||||
}
|
||||
}
|
||||
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An unhandled error occured while handling container attaching");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogTrace("Attach loop exited");
|
||||
});
|
||||
}
|
||||
|
||||
public async Task WriteInputAsync(string value)
|
||||
{
|
||||
if (Stream == null)
|
||||
throw new AggregateException("Stream is not available. Container might not be attached");
|
||||
|
||||
var buffer = Encoding.UTF8.GetBytes(value);
|
||||
await Stream.WriteAsync(buffer, 0, buffer.Length, Cts.Token);
|
||||
}
|
||||
|
||||
public async Task ClearCacheAsync()
|
||||
{
|
||||
await CacheLock.WaitAsync(Cts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
Cache.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
CacheLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string[]> GetCacheAsync()
|
||||
{
|
||||
await CacheLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
return Cache.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
CacheLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Cts.CancelAsync();
|
||||
Stream?.Dispose();
|
||||
CacheLock.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerEventService : BackgroundService
|
||||
{
|
||||
public event Func<ContainerDieEvent, Task>? OnContainerDied;
|
||||
|
||||
private readonly ILogger<DockerEventService> Logger;
|
||||
private readonly DockerClient DockerClient;
|
||||
|
||||
public DockerEventService(
|
||||
ILogger<DockerEventService> logger,
|
||||
DockerClient dockerClient
|
||||
)
|
||||
{
|
||||
Logger = logger;
|
||||
DockerClient = dockerClient;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
Logger.LogTrace("Starting up docker event monitor");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("Monitoring events");
|
||||
|
||||
await DockerClient.System.MonitorEventsAsync(
|
||||
new ContainerEventsParameters(),
|
||||
new Progress<Message>(OnEventAsync),
|
||||
stoppingToken
|
||||
);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An error occured while processing container event monitoring");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogTrace("Closed docker event monitor");
|
||||
}
|
||||
|
||||
private async void OnEventAsync(Message message)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (message.Type)
|
||||
{
|
||||
case "container":
|
||||
|
||||
var containerId = message.Actor.ID;
|
||||
|
||||
switch (message.Action)
|
||||
{
|
||||
case "die":
|
||||
|
||||
if (
|
||||
!message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr) ||
|
||||
!int.TryParse(exitCodeStr, out var exitCode)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (OnContainerDied != null)
|
||||
await OnContainerDied.Invoke(new ContainerDieEvent(containerId, exitCode));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(
|
||||
e,
|
||||
"An error occured while handling event {type} for {action}",
|
||||
message.Type,
|
||||
message.Action
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerInstallEnv : IInstallEnvironment
|
||||
{
|
||||
public IInstallStatistics Statistics => InnerStatistics;
|
||||
public IInstallConsole Console => InnerConsole;
|
||||
|
||||
public event Func<Task>? OnExited;
|
||||
|
||||
public string ContainerId { get; }
|
||||
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ILogger Logger;
|
||||
private readonly DockerEventService EventService;
|
||||
|
||||
private readonly DockerStatistics InnerStatistics;
|
||||
private readonly DockerConsole InnerConsole;
|
||||
|
||||
public DockerInstallEnv(string containerId, DockerClient dockerClient, ILogger logger, DockerEventService eventService)
|
||||
{
|
||||
ContainerId = containerId;
|
||||
DockerClient = dockerClient;
|
||||
Logger = logger;
|
||||
EventService = eventService;
|
||||
|
||||
InnerStatistics = new DockerStatistics();
|
||||
InnerConsole = new DockerConsole(containerId, dockerClient, logger);
|
||||
|
||||
EventService.OnContainerDied += HandleDieEventAsync;
|
||||
}
|
||||
|
||||
public async Task<bool> IsRunningAsync()
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
|
||||
return container.State.Running;
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
=> await DockerClient.Containers.StartContainerAsync(ContainerId, new());
|
||||
|
||||
public async Task KillAsync()
|
||||
=> await DockerClient.Containers.KillContainerAsync(ContainerId, new());
|
||||
|
||||
private async Task HandleDieEventAsync(ContainerDieEvent dieEvent)
|
||||
{
|
||||
if(dieEvent.ContainerId != ContainerId)
|
||||
return;
|
||||
|
||||
if(OnExited != null)
|
||||
await OnExited.Invoke();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
EventService.OnContainerDied -= HandleDieEventAsync;
|
||||
|
||||
await InnerConsole.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerInstallEnvService : IInstallEnvironmentService
|
||||
{
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly DockerEventService DockerEventService;
|
||||
|
||||
private const string NameTemplate = "ml-install-{0}";
|
||||
|
||||
public DockerInstallEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory,
|
||||
DockerEventService dockerEventService)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
LoggerFactory = loggerFactory;
|
||||
DockerEventService = dockerEventService;
|
||||
}
|
||||
|
||||
public async Task<IInstallEnvironment?> FindAsync(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
string.Format(NameTemplate, id)
|
||||
);
|
||||
|
||||
var logger =
|
||||
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
|
||||
|
||||
return new DockerInstallEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IInstallEnvironment> CreateAsync(
|
||||
string id,
|
||||
RuntimeConfiguration runtimeConfiguration,
|
||||
InstallConfiguration installConfiguration,
|
||||
IInstallStorage installStorage,
|
||||
IRuntimeStorage runtimeStorage
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
string.Format(NameTemplate, id)
|
||||
);
|
||||
|
||||
if (dockerInspect.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
var runtimeStoragePath = await runtimeStorage.GetHostPathAsync();
|
||||
var installStoragePath = await installStorage.GetHostPathAsync();
|
||||
|
||||
var parameters = ConfigMapper.GetInstallConfig(
|
||||
id,
|
||||
string.Format(NameTemplate, id),
|
||||
runtimeConfiguration,
|
||||
installConfiguration,
|
||||
runtimeStoragePath,
|
||||
installStoragePath
|
||||
);
|
||||
|
||||
var container = await DockerClient.Containers.CreateContainerAsync(parameters);
|
||||
|
||||
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
|
||||
|
||||
return new DockerInstallEnv(container.ID, DockerClient, logger, DockerEventService);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(IInstallEnvironment environment)
|
||||
{
|
||||
if (environment is not DockerInstallEnv dockerInstallEnv)
|
||||
throw new ArgumentException(
|
||||
$"You cannot delete runtime environments which haven't been created by {nameof(DockerInstallEnv)}");
|
||||
|
||||
await dockerInstallEnv.DisposeAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
dockerInstallEnv.ContainerId
|
||||
);
|
||||
|
||||
if (dockerInspect.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerRuntimeEnv : IRuntimeEnvironment
|
||||
{
|
||||
public IRuntimeStatistics Statistics => InnerStatistics;
|
||||
public IRuntimeConsole Console => InnerConsole;
|
||||
|
||||
public string ContainerId { get; }
|
||||
|
||||
public event Func<Task>? OnExited;
|
||||
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly DockerEventService EventService;
|
||||
|
||||
private readonly DockerConsole InnerConsole;
|
||||
private readonly DockerStatistics InnerStatistics;
|
||||
|
||||
public DockerRuntimeEnv(string containerId, DockerClient dockerClient, ILogger logger, DockerEventService eventService)
|
||||
{
|
||||
ContainerId = containerId;
|
||||
DockerClient = dockerClient;
|
||||
EventService = eventService;
|
||||
|
||||
InnerStatistics = new DockerStatistics();
|
||||
InnerConsole = new DockerConsole(containerId, dockerClient, logger);
|
||||
|
||||
EventService.OnContainerDied += HandleDieEventAsync;
|
||||
}
|
||||
|
||||
public async Task<bool> IsRunningAsync()
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
|
||||
return container.State.Running;
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
=> await DockerClient.Containers.StartContainerAsync(ContainerId, new());
|
||||
|
||||
public async Task KillAsync()
|
||||
=> await DockerClient.Containers.KillContainerAsync(ContainerId, new());
|
||||
|
||||
private async Task HandleDieEventAsync(ContainerDieEvent dieEvent)
|
||||
{
|
||||
if(dieEvent.ContainerId != ContainerId)
|
||||
return;
|
||||
|
||||
if(OnExited != null)
|
||||
await OnExited.Invoke();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
EventService.OnContainerDied -= HandleDieEventAsync;
|
||||
|
||||
await InnerConsole.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerRuntimeEnvService : IRuntimeEnvironmentService
|
||||
{
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly DockerEventService DockerEventService;
|
||||
|
||||
private const string NameTemplate = "ml-runtime-{0}";
|
||||
|
||||
public DockerRuntimeEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory,
|
||||
DockerEventService dockerEventService)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
LoggerFactory = loggerFactory;
|
||||
DockerEventService = dockerEventService;
|
||||
}
|
||||
|
||||
public async Task<IRuntimeEnvironment?> FindAsync(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
string.Format(NameTemplate, id)
|
||||
);
|
||||
|
||||
var logger =
|
||||
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
|
||||
|
||||
return new DockerRuntimeEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IRuntimeEnvironment> CreateAsync(
|
||||
string id,
|
||||
RuntimeConfiguration configuration,
|
||||
IRuntimeStorage storage
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
string.Format(NameTemplate, id)
|
||||
);
|
||||
|
||||
if (dockerInspect.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
var storagePath = await storage.GetHostPathAsync();
|
||||
|
||||
var parameters = ConfigMapper.GetRuntimeConfig(
|
||||
id,
|
||||
string.Format(NameTemplate, id),
|
||||
configuration,
|
||||
storagePath
|
||||
);
|
||||
|
||||
var container = await DockerClient.Containers.CreateContainerAsync(parameters);
|
||||
|
||||
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
|
||||
|
||||
return new DockerRuntimeEnv(container.ID, DockerClient, logger, DockerEventService);
|
||||
}
|
||||
|
||||
public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(IRuntimeEnvironment environment)
|
||||
{
|
||||
if (environment is not DockerRuntimeEnv dockerRuntimeEnv)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"You cannot delete runtime environments which haven't been created by {nameof(DockerRuntimeEnvService)}"
|
||||
);
|
||||
}
|
||||
|
||||
await dockerRuntimeEnv.DisposeAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
dockerRuntimeEnv.ContainerId
|
||||
);
|
||||
|
||||
if (dockerInspect.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerStatistics : IRuntimeStatistics, IInstallStatistics
|
||||
{
|
||||
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
|
||||
|
||||
public Task AttachAsync() => Task.CompletedTask;
|
||||
|
||||
public Task ClearCacheAsync() => Task.CompletedTask;
|
||||
|
||||
public Task<ServerStatistics[]> GetCacheAsync() => Task.FromResult<ServerStatistics[]>([]);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
public record ContainerDieEvent(string ContainerId, int ExitCode);
|
||||
@@ -0,0 +1,22 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static void AddDockerServices(this IServiceCollection collection)
|
||||
{
|
||||
var client = new DockerClientBuilder()
|
||||
.WithEndpoint(new Uri("unix:///var/run/docker.sock"))
|
||||
.Build();
|
||||
|
||||
collection.AddSingleton(client);
|
||||
|
||||
collection.AddSingleton<DockerEventService>();
|
||||
collection.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
|
||||
|
||||
collection.AddSingleton<IRuntimeEnvironmentService, DockerRuntimeEnvService>();
|
||||
collection.AddSingleton<IInstallEnvironmentService, DockerInstallEnvService>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user