Implemented first iteration of the docker-based server installer. Added restore functionality for the installer. Wired up for basic installer testing
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.Daemon.Mappers;
|
||||
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
@@ -10,55 +14,264 @@ public class DockerInstaller : IInstaller
|
||||
public IAsyncObservable<object> OnExited => OnExitedSubject.ToAsyncObservable();
|
||||
public bool IsRunning { get; private set; } = false;
|
||||
|
||||
private readonly Subject<string> OnExitedSubject = new();
|
||||
private readonly Subject<Message> OnExitedSubject = new();
|
||||
|
||||
private readonly ILogger<DockerInstaller> Logger;
|
||||
private readonly DockerEventService EventService;
|
||||
private readonly IConsole Console;
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ServerContext Context;
|
||||
private readonly DockerImageService ImageService;
|
||||
private readonly IFileSystem FileSystem;
|
||||
private readonly AppConfiguration Configuration;
|
||||
private readonly ServerConfigurationMapper Mapper;
|
||||
|
||||
private string? ContainerId;
|
||||
private string? ContainerName;
|
||||
private string ContainerName;
|
||||
|
||||
private string InstallHostPath;
|
||||
|
||||
private IAsyncDisposable? ContainerEventSubscription;
|
||||
|
||||
public DockerInstaller(
|
||||
ILogger<DockerInstaller> logger,
|
||||
DockerEventService eventService
|
||||
DockerEventService eventService,
|
||||
IConsole console,
|
||||
DockerClient dockerClient,
|
||||
ServerContext context,
|
||||
DockerImageService imageService,
|
||||
IFileSystem fileSystem,
|
||||
AppConfiguration configuration,
|
||||
ServerConfigurationMapper mapper
|
||||
)
|
||||
{
|
||||
Logger = logger;
|
||||
EventService = eventService;
|
||||
Console = console;
|
||||
DockerClient = dockerClient;
|
||||
Context = context;
|
||||
ImageService = imageService;
|
||||
FileSystem = fileSystem;
|
||||
Configuration = configuration;
|
||||
Mapper = mapper;
|
||||
}
|
||||
|
||||
public Task Initialize()
|
||||
public async Task Initialize()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
ContainerName = $"moonlight-install-{Context.Configuration.Id}";
|
||||
InstallHostPath =
|
||||
Path.GetFullPath(Path.Combine(Configuration.Storage.Install, Context.Configuration.Id.ToString()));
|
||||
|
||||
ContainerEventSubscription = await EventService
|
||||
.OnContainerEvent
|
||||
.SubscribeAsync(HandleContainerEvent);
|
||||
|
||||
// Check for any already existing runtime container to reclaim
|
||||
Logger.LogDebug("Searching for orphan container to reclaim");
|
||||
|
||||
try
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
|
||||
|
||||
ContainerId = container.ID;
|
||||
IsRunning = container.State.Running;
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask HandleContainerEvent(Message message)
|
||||
{
|
||||
// Only handle events for our own container
|
||||
if (message.ID != ContainerId)
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
// Only handle die events
|
||||
if (message.Action != "die")
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
OnExitedSubject.OnNext(message);
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Sync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async Task Setup()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// Plan of action:
|
||||
// 1. Ensure no other container with that name exist
|
||||
// 2. Ensure the docker image has been downloaded
|
||||
// 3. Create the installation volume and place script in there
|
||||
// 4. Create the container from the configuration in the meta
|
||||
|
||||
// 1. Ensure no other container with that name exist
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Searching for orphan container");
|
||||
|
||||
var possibleContainer = await DockerClient.Containers.InspectContainerAsync(ContainerName);
|
||||
|
||||
Logger.LogDebug("Orphan container found. Removing it");
|
||||
await Console.WriteToMoonlight("Found orphan container. Removing it");
|
||||
|
||||
await EnsureContainerOffline(possibleContainer);
|
||||
|
||||
Logger.LogInformation("Removing orphan container");
|
||||
await DockerClient.Containers.RemoveContainerAsync(ContainerName, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
// 2. Ensure the docker image has been downloaded
|
||||
await Console.WriteToMoonlight("Downloading docker image");
|
||||
|
||||
await ImageService.Download(Context.Configuration.DockerImage, async message =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Console.WriteToMoonlight(message);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignored. Not handling it here could cause an application wide crash afaik
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Create the installation volume and place script in there
|
||||
|
||||
await Console.WriteToMoonlight("Creating storage");
|
||||
|
||||
if(Directory.Exists(InstallHostPath))
|
||||
Directory.Delete(InstallHostPath, true);
|
||||
|
||||
Directory.CreateDirectory(InstallHostPath);
|
||||
|
||||
await File.WriteAllTextAsync(Path.Combine(InstallHostPath, "install.sh"), Context.InstallConfiguration.Script);
|
||||
|
||||
// 4. Create the container from the configuration in the meta
|
||||
var runtimeFsPath = FileSystem.GetExternalPath();
|
||||
|
||||
var parameters = Mapper.ToInstallParameters(
|
||||
Context.Configuration,
|
||||
Context.InstallConfiguration,
|
||||
runtimeFsPath,
|
||||
InstallHostPath,
|
||||
ContainerName
|
||||
);
|
||||
|
||||
var createdContainer = await DockerClient.Containers.CreateContainerAsync(parameters);
|
||||
|
||||
ContainerId = createdContainer.ID;
|
||||
|
||||
Logger.LogInformation("Created container");
|
||||
await Console.WriteToMoonlight("Created container");
|
||||
}
|
||||
|
||||
public Task Start()
|
||||
public async Task Start()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
Logger.LogInformation("Starting container");
|
||||
await Console.WriteToMoonlight("Starting container");
|
||||
await DockerClient.Containers.StartContainerAsync(ContainerId, new());
|
||||
}
|
||||
|
||||
public Task Abort()
|
||||
public async Task Abort()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
await EnsureContainerOffline();
|
||||
}
|
||||
|
||||
public Task Cleanup()
|
||||
public async Task Cleanup()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// Plan of action:
|
||||
// 1. Search for the container by id or name
|
||||
// 2. Ensure container is offline
|
||||
// 3. Remove the container
|
||||
// 4. Delete installation volume if it exists
|
||||
|
||||
// 1. Search for the container by id or name
|
||||
ContainerInspectResponse? container = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(ContainerId))
|
||||
container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
|
||||
else
|
||||
container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
|
||||
Logger.LogDebug("Runtime container could not be found. Reporting deprovision success");
|
||||
}
|
||||
|
||||
// No container found? We are done here then
|
||||
if (container == null)
|
||||
return;
|
||||
|
||||
// 2. Ensure container is offline
|
||||
await EnsureContainerOffline(container);
|
||||
|
||||
// 3. Remove the container
|
||||
Logger.LogInformation("Removing container");
|
||||
await Console.WriteToMoonlight("Removing container");
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(container.ID, new());
|
||||
|
||||
// 4. Delete installation volume if it exists
|
||||
|
||||
if (Directory.Exists(InstallHostPath))
|
||||
{
|
||||
Logger.LogInformation("Removing storage");
|
||||
await Console.WriteToMoonlight("Removing storage");
|
||||
|
||||
Directory.Delete(InstallHostPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<ServerCrash?> SearchForCrash()
|
||||
public async Task<ServerCrash?> SearchForCrash()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task EnsureContainerOffline(ContainerInspectResponse? container = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(ContainerId))
|
||||
container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
|
||||
else
|
||||
container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
Logger.LogDebug("No container found to ensure its offline");
|
||||
|
||||
// Ignored
|
||||
}
|
||||
|
||||
// No container found? We are done here then
|
||||
if (container == null)
|
||||
return;
|
||||
|
||||
// Check if container is running
|
||||
if (!container.State.Running)
|
||||
return;
|
||||
|
||||
await Console.WriteToMoonlight("Killing container");
|
||||
await DockerClient.Containers.KillContainerAsync(ContainerId, new());
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
OnExitedSubject.Dispose();
|
||||
|
||||
if (ContainerEventSubscription != null)
|
||||
await ContainerEventSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user