Skip to content

Commit

Permalink
dockercompose support and readme updates
Browse files Browse the repository at this point in the history
  • Loading branch information
cruikshj committed May 3, 2024
1 parent 52aaf0a commit d36a985
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 13 deletions.
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ This repository represents a Discord Bot application for managing dedicated game

- Provides server readme through using interactions
- Provides server files (such as backups, saves and mod files) using interactions
- Kubernetes integration
- Start/Stop deployments and stateful sets using interactions
- Provides pod logs using interactions
- Process, DockerCompose, and Kubernetes integrations
- Show server status
- Start/Stop servers using interactions
- Provides server logs using interactions

## Setup

Expand Down Expand Up @@ -107,8 +108,9 @@ The `BuiltInLargeFileDownloadHandler` will create a temporary download link usin

| Section | Description | Default |
|---|---|---|
| -Key- | (Required) The section key or name is used to lookup the adapter matching the `ServerHostAdapter` value on server info. | |
| Type | (Required) The type of the adapter. Can be `Kubernetes`. | |
| -Key- | (Required) The section key or name is used to lookup the adapter matching the `HostAdapter` value on server info. | |
| Type | (Required) The type of the adapter. Can be `Process`, `DockerCompose`, or `Kubernetes`. | |
| DockerProcessFilePath (DockerCompose) | The file name of the docker executable. | docker |
| KubeConfigPath (Kubernetes) | The path to a Kube Config file to use to connect to Kubernetes. If not provided, `InCluster` configuration will be used. | |

##### Servers
Expand All @@ -121,8 +123,15 @@ The `BuiltInLargeFileDownloadHandler` will create a temporary download link usin
| Readme | A multiline text field that can be provided by bot interaction. This value supports string formatting using the server info object in the form of "{Game}" or "{Fields.Whatever}" | |
| FilesPath | This is a path to the files directory to be used to server files through interactions for this server. It should be a file path a mount drive for the bot container. The idea here is to utilize existing volume mount capabilities of hosting platforms like Docker and Kubernetes, to mount in whatever is necessary, such as an S3 bucket or NFS or host drive. | |
| Fields | This is a map of free form fields, or key value pairs to display as part of the server info. | |
| ServerHostAdapter | The key for the `ServerHostAdapter` to use for this server. | |
| ServerHostIdentifier | The identifier to pass to the adapter. The format is determined by the adapter. | |
| HostAdapter | The key for the `ServerHostAdapter` to use for this server. | |
| HostProperties | The identifier to pass to the adapter. The child properties are determined by the adapter. | |
| HostProperties.FileName (Process) | The process filename. | |
| HostProperties.Arguments (Process) | The process arguments. | |
| HostProperties.WorkingDirectory (Process) | The working directory to use when starting the process. | |
| HostProperties.DockerComposeFilePath (DockerCompose) | The file path to the docker compose configuration file for the server. | |
| HostProperties.Kind (Kubernetes) | The workload kind. Can be `Deployment` or `StatefulSet`. | |
| HostProperties.Namespace (Kubernetes) | The workload namespace. | |
| HostProperties.Name (Kubernetes) | The workload name. | |

## Contribution

Expand Down
10 changes: 10 additions & 0 deletions src/ServerManager.DiscordBot/Extensions/DisposableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
public static class DisposableExtensions
{
public static void DisposeAll(this IEnumerable<IDisposable> disposables)
{
foreach (var disposable in disposables)
{
disposable.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System.Diagnostics;
using System.Text;
using Microsoft.Extensions.Options;

public class DockerComposeServerHostAdapter(
IOptions<DockerComposeServerHostAdapterOptions> options)
: ServerHostAdapterBase<DockerComposeServerHostProperties>
{
public DockerComposeServerHostAdapterOptions Options { get; } = options.Value;

public override async Task<ServerStatus> GetServerStatusAsync(CancellationToken cancellationToken = default)
{
using var process = StartDockerComposeProcess("ps --latest --format \"{{.Status}}\"");

var output = await process.StandardOutput.ReadToEndAsync();

await process.WaitForExitAsync(cancellationToken);

if (process.ExitCode != 0)
{
throw new Exception($"Failed to get server status. Exit code: {process.ExitCode}");
}

var status = output.Split(' ')[0];

return status switch
{
"Up" => ServerStatus.Running,
"Exited" => ServerStatus.Stopped,
_ => ServerStatus.Unknown
};
}

public override async Task StartServerAsync(CancellationToken cancellationToken = default)
{
using var process = StartDockerComposeProcess("up -d");

await process.WaitForExitAsync(cancellationToken);

if (process.ExitCode != 0)
{
throw new Exception($"Failed to start server. Exit code: {process.ExitCode}");
}
}

public override async Task StopServerAsync(CancellationToken cancellationToken = default)
{
using var process = StartDockerComposeProcess("down");

await process.WaitForExitAsync(cancellationToken);

if (process.ExitCode != 0)
{
throw new Exception($"Failed to stop server. Exit code: {process.ExitCode}");
}
}

public override async Task<IDictionary<string, Stream>> GetServerLogsAsync(CancellationToken cancellationToken = default)
{
using var process = StartDockerComposeProcess("logs");

var logs = await process.StandardOutput.ReadToEndAsync(cancellationToken);

await process.WaitForExitAsync(cancellationToken);

if (process.ExitCode != 0)
{
throw new Exception($"Failed to get server logs. Exit code: {process.ExitCode}");
}

return new Dictionary<string, Stream>
{
{ "output", new MemoryStream(Encoding.UTF8.GetBytes(logs)) }
};
}

private Process StartDockerComposeProcess(string arguments)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = Options.DockerProcessFilePath,
Arguments = $"compose -f {Context.Properties.DockerComposeFilePath} {arguments}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};

process.Start();

return process;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;

public class DockerComposeServerHostAdapterOptions : IOptions<DockerComposeServerHostAdapterOptions>
{
[Required]
public string DockerProcessFilePath { get; set; } = "docker";

DockerComposeServerHostAdapterOptions IOptions<DockerComposeServerHostAdapterOptions>.Value => this;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.ComponentModel.DataAnnotations;

public class DockerComposeServerHostProperties
{
[Required]
public string DockerComposeFilePath { get; set; } = default!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public override Task<ServerStatus> GetServerStatusAsync(CancellationToken cancel
? ServerStatus.Running
: ServerStatus.Stopped;

processes.DisposeAll();

return Task.FromResult(status);
}

Expand All @@ -27,7 +29,7 @@ public override async Task StartServerAsync(CancellationToken cancellationToken
throw new InvalidOperationException("Server is already running.");
}

var process = new Process
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
Expand Down Expand Up @@ -59,13 +61,17 @@ public override async Task StopServerAsync(CancellationToken cancellationToken =
{
process.Kill();
}

processes.DisposeAll();
}

public override Task<IDictionary<string, Stream>> GetServerLogsAsync(CancellationToken cancellationToken = default)
public override async Task<IDictionary<string, Stream>> GetServerLogsAsync(CancellationToken cancellationToken = default)
{
var processName = Path.GetFileName(Context.Properties.FileName);

var process = Process.GetProcessesByName(processName).FirstOrDefault();
var processes = Process.GetProcessesByName(processName);

var process = processes.FirstOrDefault();

var logs = new Dictionary<string, Stream>();

Expand All @@ -74,7 +80,7 @@ public override Task<IDictionary<string, Stream>> GetServerLogsAsync(Cancellatio
var outputStream = new MemoryStream();
using (var writer = new StreamWriter(outputStream, leaveOpen: true))
{
writer.Write(process.StandardOutput.ReadToEnd());
writer.Write(await process.StandardOutput.ReadToEndAsync());
}
if (outputStream.Length > 0)
{
Expand All @@ -85,7 +91,7 @@ public override Task<IDictionary<string, Stream>> GetServerLogsAsync(Cancellatio
var errorStream = new MemoryStream();
using (var writer = new StreamWriter(errorStream, leaveOpen: true))
{
writer.Write(process.StandardError.ReadToEnd());
writer.Write(await process.StandardError.ReadToEndAsync());
}
if (errorStream.Length > 0)
{
Expand All @@ -94,6 +100,8 @@ public override Task<IDictionary<string, Stream>> GetServerLogsAsync(Cancellatio
}
}

return Task.FromResult<IDictionary<string, Stream>>(logs);
processes.DisposeAll();

return logs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ public static WebApplicationBuilder ConfigureServerHostAdapters(this WebApplicat
return new ProcessServerHostAdapter(options);
});
break;
case ServerHosterAdapterType.DockerCompose:
builder.Services.Configure<DockerComposeServerHostAdapterOptions>(key, child);
builder.Services.AddKeyedTransient<IServerHostAdapter, DockerComposeServerHostAdapter>(key, (sp, sk) =>
{
var options = sp.GetRequiredService<IOptionsSnapshot<DockerComposeServerHostAdapterOptions>>().Get(key);
return new DockerComposeServerHostAdapter(options);
});
break;
case ServerHosterAdapterType.Kubernetes:
builder.Services.Configure<KubernetesServerHostAdapterOptions>(key, child);
builder.Services.AddKeyedTransient<IServerHostAdapter, KubernetesServerHostAdapter>(key, (sp, sk) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
public enum ServerHosterAdapterType
{
Process,
DockerCompose,
Kubernetes
}

0 comments on commit d36a985

Please sign in to comment.