Added node server sync and delete sync. Cleaned up codebase and extracted calls to apis to services

This commit is contained in:
2025-03-02 19:24:24 +01:00
parent ef7f866ded
commit 30390dab71
25 changed files with 751 additions and 282 deletions

View File

@@ -8,6 +8,7 @@ using MoonCore.Helpers;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Requests.Admin.Servers;
using MoonlightServers.Shared.Http.Responses.Admin.Servers;
@@ -24,6 +25,8 @@ public class ServersController : Controller
private readonly DatabaseRepository<ServerVariable> VariableRepository;
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly ILogger<ServersController> Logger;
private readonly ServerService ServerService;
public ServersController(
CrudHelper<Server, ServerDetailResponse> crudHelper,
@@ -32,7 +35,10 @@ public class ServersController : Controller
DatabaseRepository<Allocation> allocationRepository,
DatabaseRepository<ServerVariable> variableRepository,
DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository)
DatabaseRepository<User> userRepository,
ILogger<ServersController> logger,
ServerService serverService
)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
@@ -41,6 +47,8 @@ public class ServersController : Controller
VariableRepository = variableRepository;
ServerRepository = serverRepository;
UserRepository = userRepository;
ServerService = serverService;
Logger = logger;
CrudHelper.QueryModifier = servers => servers
.Include(x => x.Node)
@@ -162,10 +170,23 @@ public class ServersController : Controller
server.Node = node;
server.Star = star;
// TODO: Call node
var finalServer = await ServerRepository.Add(server);
try
{
await ServerService.Sync(finalServer);
}
catch (Exception e)
{
Logger.LogError("Unable to sync server to node the server is assigned to: {e}", e);
// We are deleting the server from the database after the creation has failed
// to ensure we wont have a bugged server in the database which doesnt exist on the node
await ServerRepository.Remove(finalServer);
throw;
}
return CrudHelper.MapToResult(finalServer);
}
@@ -215,23 +236,45 @@ public class ServersController : Controller
var serverVar = server.Variables
.FirstOrDefault(x => x.Key == variable.Key);
if(serverVar == null)
if (serverVar == null)
continue;
// Update value
serverVar.Value = variable.Value;
}
// TODO: Call node
await ServerRepository.Update(server);
// Notify the node about the changes
await ServerService.Sync(server);
return CrudHelper.MapToResult(server);
}
[HttpDelete("{id:int}")]
public async Task Delete([FromRoute] int id)
public async Task Delete([FromRoute] int id, [FromQuery] bool force = false)
{
var server = await CrudHelper.GetSingleModel(id);
try
{
// If the sync fails on the node and we aren't forcing the deletion,
// we don't want to delete it from the database yet
await ServerService.SyncDelete(server);
}
catch (Exception e)
{
if (force)
{
Logger.LogWarning(
"An error occured while syncing deletion of a server to the node. Continuing anyways. Error: {e}",
e
);
}
else
throw;
}
await CrudHelper.Delete(id);
}
}

View File

@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
@@ -15,113 +16,69 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
public class ServerPowerController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly NodeService NodeService;
private readonly DatabaseRepository<User> UserRepository;
private readonly ServerService ServerService;
public ServerPowerController(DatabaseRepository<Server> serverRepository, NodeService nodeService)
public ServerPowerController(
DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository,
ServerService serverService
)
{
ServerRepository = serverRepository;
NodeService = nodeService;
UserRepository = userRepository;
ServerService = serverService;
}
[HttpPost("{serverId:int}/start")]
[Authorize]
public async Task Start([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(serverId);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/start");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
var server = await GetServerById(serverId);
await ServerService.Start(server);
}
[HttpPost("{serverId:int}/stop")]
[Authorize]
public async Task Stop([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(serverId);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/stop");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
var server = await GetServerById(serverId);
await ServerService.Stop(server);
}
[HttpPost("{serverId:int}/kill")]
[Authorize]
public async Task Kill([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(serverId);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/kill");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
var server = await GetServerById(serverId);
await ServerService.Kill(server);
}
[HttpPost("{serverId:int}/install")]
[Authorize]
public async Task Install([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(serverId);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/install");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
var server = await GetServerById(serverId);
await ServerService.Install(server);
}
private async Task<Server> GetServerWithPermCheck(int serverId,
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
private async Task<Server> GetServerById(int serverId)
{
var userIdClaim = User.Claims.First(x => x.Type == "userId");
var userId = int.Parse(userIdClaim.Value);
var query = ServerRepository
var server = await ServerRepository
.Get()
.Include(x => x.Node) as IQueryable<Server>;
if (queryModifier != null)
query = queryModifier.Invoke(query);
var server = await query
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
if (server.OwnerId == userId) // The current user is the owner
return server;
var permissions = User.Claims.First(x => x.Type == "permissions").Value.Split(";", StringSplitOptions.RemoveEmptyEntries);
if (PermissionHelper.HasPermission(permissions, "admin.servers.get")) // The current user is an admin
return server;
var userIdClaim = User.Claims.First(x => x.Type == "userId");
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if (!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404);
return server;
}
}

View File

@@ -3,8 +3,8 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Extensions;
using MoonlightServers.ApiServer.Services;
@@ -17,13 +17,17 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Route("api/client/servers")]
public class ServersController : Controller
{
private readonly ServerService ServerService;
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly NodeService NodeService;
public ServersController(DatabaseRepository<Server> serverRepository, NodeService nodeService)
public ServersController(DatabaseRepository<Server> serverRepository, NodeService nodeService, ServerService serverService, DatabaseRepository<User> userRepository)
{
ServerRepository = serverRepository;
NodeService = nodeService;
ServerService = serverService;
UserRepository = userRepository;
}
[HttpGet]
@@ -71,13 +75,22 @@ public class ServersController : Controller
[Authorize]
public async Task<ServerDetailResponse> Get([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(
serverId,
query =>
query
var server = await ServerRepository
.Get()
.Include(x => x.Allocations)
.Include(x => x.Star)
);
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if(server == null)
throw new HttpApiException("No server with this id found", 404);
var userIdClaim = User.Claims.First(x => x.Type == "userId");
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if(!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404);
return new ServerDetailResponse()
{
@@ -98,32 +111,20 @@ public class ServersController : Controller
[Authorize]
public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(serverId);
var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
var data = await apiClient.GetJson<DaemonShared.DaemonSide.Http.Responses.Servers.ServerStatusResponse>(
$"api/servers/{server.Id}/status"
);
var server = await GetServerById(serverId);
var status = await ServerService.GetStatus(server);
return new ServerStatusResponse()
{
State = data.State.ToServerPowerState()
State = status.State.ToServerPowerState()
};
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
[HttpGet("{serverId:int}/ws")]
[Authorize]
public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(serverId);
var server = await GetServerById(serverId);
// TODO: Handle transparent node proxy
@@ -135,11 +136,7 @@ public class ServersController : Controller
var url = "";
if (server.Node.UseSsl)
url += "https://";
else
url += "http://";
url += server.Node.UseSsl ? "https://" : "http://";
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/ws";
return new ServerWebSocketResponse()
@@ -153,54 +150,33 @@ public class ServersController : Controller
[Authorize]
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(serverId);
var server = await GetServerById(serverId);
var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
var data = await apiClient.GetJson<DaemonShared.DaemonSide.Http.Responses.Servers.ServerLogsResponse>(
$"api/servers/{server.Id}/logs"
);
var logs = await ServerService.GetLogs(server);
return new ServerLogsResponse()
{
Messages = data.Messages
Messages = logs.Messages
};
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
private async Task<Server> GetServerWithPermCheck(int serverId,
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
private async Task<Server> GetServerById(int serverId)
{
var userIdClaim = User.Claims.First(x => x.Type == "userId");
var userId = int.Parse(userIdClaim.Value);
var query = ServerRepository
var server = await ServerRepository
.Get()
.Include(x => x.Node) as IQueryable<Server>;
if (queryModifier != null)
query = queryModifier.Invoke(query);
var server = await query
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
if(server == null)
throw new HttpApiException("No server with this id found", 404);
if (server.OwnerId == userId) // The current user is the owner
return server;
var permissions = User.Claims.First(x => x.Type == "permissions").Value.Split(";", StringSplitOptions.RemoveEmptyEntries);
if (PermissionHelper.HasPermission(permissions, "admin.servers.get")) // The current user is an admin
return server;
var userIdClaim = User.Claims.First(x => x.Type == "userId");
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if(!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404);
return server;
}
}

View File

@@ -58,47 +58,12 @@ public class ServersController : Controller
foreach (var server in servers)
{
var dockerImage = server.Star.DockerImages
.Skip(server.DockerImageIndex)
.FirstOrDefault();
var convertedData = ConvertToServerData(server);
if (dockerImage == null)
{
dockerImage = server.Star.DockerImages
.Skip(server.Star.DefaultDockerImage)
.FirstOrDefault();
}
if (dockerImage == null)
dockerImage = server.Star.DockerImages.LastOrDefault();
if (dockerImage == null)
{
Logger.LogWarning("Unable to map server data for server {id}: No docker image available", server.Id);
if (convertedData == null)
continue;
}
serverData.Add(new ServerDataResponse()
{
Id = server.Id,
StartupCommand = server.StartupOverride ?? server.Star.StartupCommand,
Allocations = server.Allocations.Select(x => new AllocationDataResponse()
{
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray(),
Variables = server.Variables.ToDictionary(x => x.Key, x => x.Value),
Bandwidth = server.Bandwidth,
Cpu = server.Cpu,
Disk = server.Disk,
Memory = server.Memory,
OnlineDetection = server.Star.OnlineDetection,
DockerImage = dockerImage.Identifier,
PullDockerImage = dockerImage.AutoPulling,
ParseConiguration = server.Star.ParseConfiguration,
StopCommand = server.Star.StopCommand,
UseVirtualDisk = server.UseVirtualDisk
});
serverData.Add(convertedData);
}
return new PagedData<ServerDataResponse>()
@@ -111,6 +76,38 @@ public class ServersController : Controller
};
}
[HttpGet("{id:int}")]
public async Task<ServerDataResponse> Get([FromRoute] int id)
{
// Load the node via the token id
var tokenId = User.Claims.First(x => x.Type == "iss").Value;
var node = await NodeRepository
.Get()
.FirstAsync(x => x.TokenId == tokenId);
// Load the server with the star data attached. We filter by the node to ensure the node can only access
// servers linked to it
var server = await ServerRepository
.Get()
.Where(x => x.Node.Id == node.Id)
.Include(x => x.Star)
.ThenInclude(x => x.DockerImages)
.Include(x => x.Variables)
.Include(x => x.Allocations)
.FirstOrDefaultAsync(x => x.Id == id);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var convertedData = ConvertToServerData(server);
if (convertedData == null)
throw new HttpApiException("An error occured while creating the server data model", 500);
return convertedData;
}
[HttpGet("{id:int}/install")]
public async Task<ServerInstallDataResponse> GetInstall([FromRoute] int id)
{
@@ -139,4 +136,58 @@ public class ServersController : Controller
Shell = server.Star.InstallShell
};
}
private ServerDataResponse? ConvertToServerData(Server server)
{
// Find the docker image to use for this server
StarDockerImage? dockerImage = null;
// Handle server set image if specified
if (server.DockerImageIndex != -1)
{
dockerImage = server.Star.DockerImages
.Skip(server.DockerImageIndex)
.FirstOrDefault();
}
// Handle star default image if set
if (dockerImage == null && server.Star.DefaultDockerImage != -1)
{
dockerImage = server.Star.DockerImages
.Skip(server.Star.DefaultDockerImage)
.FirstOrDefault();
}
if (dockerImage == null)
dockerImage = server.Star.DockerImages.LastOrDefault();
if (dockerImage == null)
{
Logger.LogWarning("Unable to map server data for server {id}: No docker image available", server.Id);
return null;
}
// Convert model
return new ServerDataResponse()
{
Id = server.Id,
StartupCommand = server.StartupOverride ?? server.Star.StartupCommand,
Allocations = server.Allocations.Select(x => new AllocationDataResponse()
{
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray(),
Variables = server.Variables.ToDictionary(x => x.Key, x => x.Value),
Bandwidth = server.Bandwidth,
Cpu = server.Cpu,
Disk = server.Disk,
Memory = server.Memory,
OnlineDetection = server.Star.OnlineDetection,
DockerImage = dockerImage.Identifier,
PullDockerImage = dockerImage.AutoPulling,
ParseConiguration = server.Star.ParseConfiguration,
StopCommand = server.Star.StopCommand,
UseVirtualDisk = server.UseVirtualDisk
};
}
}

View File

@@ -14,27 +14,6 @@ namespace MoonlightServers.ApiServer.Services;
[Singleton]
public class NodeService
{
public async Task<HttpApiClient> CreateApiClient(Node node)
{
var url = "";
if (node.UseSsl)
url += "https://";
else
url += "http://";
url += $"{node.Fqdn}:{node.HttpPort}/";
var httpClient = new HttpClient()
{
BaseAddress = new Uri(url)
};
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {node.Token}");
return new HttpApiClient(httpClient);
}
public string CreateAccessToken(Node node, Action<Dictionary<string, object>> parameters, TimeSpan duration)
{
var claims = new Dictionary<string, object>();
@@ -63,7 +42,7 @@ public class NodeService
public async Task<SystemStatusResponse> GetSystemStatus(Node node)
{
using var apiClient = await CreateApiClient(node);
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<SystemStatusResponse>("api/system/status");
}
@@ -71,21 +50,66 @@ public class NodeService
public async Task<StatisticsApplicationResponse> GetApplicationStatistics(Node node)
{
using var apiClient = await CreateApiClient(node);
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsApplicationResponse>("api/statistics/application");
}
public async Task<StatisticsHostResponse> GetHostStatistics(Node node)
{
using var apiClient = await CreateApiClient(node);
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsHostResponse>("api/statistics/host");
}
public async Task<StatisticsDockerResponse> GetDockerStatistics(Node node)
{
using var apiClient = await CreateApiClient(node);
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsDockerResponse>("api/statistics/docker");
}
#endregion
#region Helpers
private string GenerateJwt(Node node)
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
//Expires = DateTime.UtcNow.AddYears(1),
Expires = DateTime.UtcNow.AddMinutes(1),
NotBefore = DateTime.UtcNow.AddSeconds(-1),
IssuedAt = DateTime.UtcNow,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
node.Token
)),
SecurityAlgorithms.HmacSha256
)
};
var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor);
return jwtSecurityTokenHandler.WriteToken(securityToken);
}
public HttpApiClient CreateApiClient(Node node)
{
var url = "";
url += node.UseSsl ? "https://" : "http://";
url += $"{node.Fqdn}:{node.HttpPort}/";
var httpClient = new HttpClient()
{
BaseAddress = new Uri(url)
};
var jwt = GenerateJwt(node);
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
return new HttpApiClient(httpClient);
}
#endregion
}

View File

@@ -0,0 +1,165 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
namespace MoonlightServers.ApiServer.Services;
[Scoped]
public class ServerService
{
private readonly NodeService NodeService;
private readonly DatabaseRepository<Server> ServerRepository;
public ServerService(NodeService nodeService, DatabaseRepository<Server> serverRepository)
{
NodeService = nodeService;
ServerRepository = serverRepository;
}
#region Power Actions
public async Task Start(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/start");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task Stop(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/stop");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task Kill(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/kill");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
#endregion
public async Task Install(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/install");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task Sync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/sync");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task SyncDelete(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Delete($"api/servers/{server.Id}");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task<ServerStatusResponse> GetStatus(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
return await apiClient.GetJson<ServerStatusResponse>($"api/servers/{server.Id}/status");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task<ServerLogsResponse> GetLogs(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
return await apiClient.GetJson<ServerLogsResponse>($"api/servers/{server.Id}/logs");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
#region Helpers
public bool IsAllowedToAccess(User user, Server server)
{
if (server.OwnerId == user.Id)
return true;
var permissions = JsonSerializer.Deserialize<string[]>(
user.PermissionsJson
) ?? [];
return PermissionHelper.HasPermission(permissions, "admin.servers.get");
}
private async Task<HttpApiClient> GetApiClient(Server server)
{
var serverWithNode = server;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
// It can be null when its not included when loading via ef !!!
if (server.Node == null)
{
serverWithNode = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstAsync(x => x.Id == server.Id);
}
return NodeService.CreateApiClient(serverWithNode.Node);
}
#endregion
}

View File

@@ -22,9 +22,17 @@ public partial class Server
await LogToConsole("Fetching installation configuration");
// Fetching remote configuration
// Fetching remote configuration and install config
var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
var installData = await remoteService.GetServerInstallation(Configuration.Id);
var serverData = await remoteService.GetServer(Configuration.Id);
// We are updating the regular server config here as well
// as changes to variables and other settings wouldn't sync otherwise
// because they won't trigger a sync
var serverConfiguration = serverData.ToServerConfiguration();
UpdateConfiguration(serverConfiguration);
var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>();

View File

@@ -1,5 +1,7 @@
using Docker.DotNet;
using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Extensions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Abstractions;
@@ -11,6 +13,17 @@ public partial class Server
{
try
{
await LogToConsole("Fetching configuration");
var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
var serverData = await remoteService.GetServer(Configuration.Id);
// We are updating the server config here
// as changes to variables and other settings wouldn't sync otherwise
// because they won't trigger a sync
var serverConfiguration = serverData.ToServerConfiguration();
UpdateConfiguration(serverConfiguration);
await ReCreate();
await LogToConsole("Starting container");

View File

@@ -49,4 +49,7 @@ public partial class Server
RuntimeContainerName = $"moonlight-runtime-{Configuration.Id}";
InstallationContainerName = $"moonlight-install-{Configuration.Id}";
}
public void UpdateConfiguration(ServerConfiguration configuration)
=> Configuration = configuration;
}

View File

@@ -2,11 +2,35 @@ using Docker.DotNet.Models;
using Mono.Unix.Native;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Extensions;
public static class ServerConfigurationExtensions
{
public static ServerConfiguration ToServerConfiguration(this ServerDataResponse response)
{
return new ServerConfiguration()
{
Id = response.Id,
StartupCommand = response.StartupCommand,
Allocations = response.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
{
IpAddress = y.IpAddress,
Port = y.Port
}).ToArray(),
Variables = response.Variables,
OnlineDetection = response.OnlineDetection,
DockerImage = response.DockerImage,
UseVirtualDisk = response.UseVirtualDisk,
Bandwidth = response.Bandwidth,
Cpu = response.Cpu,
Disk = response.Disk,
Memory = response.Memory,
StopCommand = response.StopCommand
};
}
public static CreateContainerParameters ToRuntimeCreateParameters(this ServerConfiguration configuration,
string hostPath, string containerName)
{

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.Enums;
@@ -5,6 +6,7 @@ using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[Authorize]
[ApiController]
[Route("api/servers")]
public class ServerPowerController : Controller

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.Services;
@@ -6,6 +7,7 @@ using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[Authorize]
[ApiController]
[Route("api/servers")]
public class ServersController : Controller
@@ -17,18 +19,32 @@ public class ServersController : Controller
ServerService = serverService;
}
[HttpPost("{serverId:int}/sync")]
public async Task Sync([FromRoute] int serverId)
{
await ServerService.Sync(serverId);
}
[HttpDelete("{serverId:int}")]
public async Task Delete([FromRoute] int serverId)
{
await ServerService.Delete(serverId);
}
[HttpGet("{serverId:int}/status")]
public async Task<ServerStatusResponse> GetStatus(int serverId)
public Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
{
var server = ServerService.GetServer(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return new ServerStatusResponse()
var result = new ServerStatusResponse()
{
State = (ServerState)server.State
};
return Task.FromResult(result);
}
[HttpGet("{serverId:int}/logs")]

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
@@ -6,6 +7,7 @@ namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
// This controller hosts endpoints for the statistics for the daemon application itself
[Authorize]
[ApiController]
[Route("api/statistics/application")]
public class StatisticsApplicationController : Controller

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
@@ -6,6 +7,7 @@ namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
// This controller hosts endpoints for the statistics for the docker environment
[Authorize]
[ApiController]
[Route("api/statistics/docker")]
public class StatisticsDockerController : Controller

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
@@ -6,6 +7,7 @@ namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
// This controller hosts endpoints for the statistics for host system the daemon runs on
[Authorize]
[ApiController]
[Route("api/statistics/host")]
public class StatisticsHostController : Controller

View File

@@ -1,10 +1,12 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Sys;
namespace MoonlightServers.Daemon.Http.Controllers.Sys;
[Authorize]
[ApiController]
[Route("api/system/status")]
public class SystemStatusController : Controller

View File

@@ -31,6 +31,13 @@ public class RemoteService
);
}
public async Task<ServerDataResponse> GetServer(int serverId)
{
return await ApiClient.GetJson<ServerDataResponse>(
$"api/remote/servers/{serverId}"
);
}
public async Task<ServerInstallDataResponse> GetServerInstallation(int serverId)
{
return await ApiClient.GetJson<ServerInstallDataResponse>(

View File

@@ -1,8 +1,11 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Models;
using MoonlightServers.Daemon.Abstractions;
using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Extensions;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
@@ -16,11 +19,15 @@ public class ServerService : IHostedLifecycleService
private readonly RemoteService RemoteService;
private readonly IServiceProvider ServiceProvider;
private readonly ILoggerFactory LoggerFactory;
private bool IsInitialized = false;
private CancellationTokenSource Cancellation = new();
private bool IsInitialized = false;
public ServerService(RemoteService remoteService, ILogger<ServerService> logger, IServiceProvider serviceProvider,
ILoggerFactory loggerFactory)
public ServerService(
RemoteService remoteService,
ILogger<ServerService> logger,
IServiceProvider serviceProvider,
ILoggerFactory loggerFactory
)
{
RemoteService = remoteService;
Logger = logger;
@@ -35,7 +42,7 @@ public class ServerService : IHostedLifecycleService
Logger.LogWarning("Ignoring initialize call: Already initialized");
return;
}
else
IsInitialized = true;
// Loading models and converting them
@@ -45,25 +52,9 @@ public class ServerService : IHostedLifecycleService
await RemoteService.GetServers(page, pageSize)
);
var configurations = servers.Select(x => new ServerConfiguration()
{
Id = x.Id,
StartupCommand = x.StartupCommand,
Allocations = x.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
{
IpAddress = y.IpAddress,
Port = y.Port
}).ToArray(),
Variables = x.Variables,
OnlineDetection = x.OnlineDetection,
DockerImage = x.DockerImage,
UseVirtualDisk = x.UseVirtualDisk,
Bandwidth = x.Bandwidth,
Cpu = x.Cpu,
Disk = x.Disk,
Memory = x.Memory,
StopCommand = x.StopCommand
}).ToArray();
var configurations = servers
.Select(x => x.ToServerConfiguration())
.ToArray();
Logger.LogInformation("Initializing {count} servers", servers.Length);
@@ -91,7 +82,7 @@ public class ServerService : IHostedLifecycleService
await Cancellation.CancelAsync();
}
private async Task AttachToDockerEvents()
private Task AttachToDockerEvents()
{
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
@@ -144,9 +135,11 @@ public class ServerService : IHostedLifecycleService
}
}
});
return Task.CompletedTask;
}
private async Task InitializeServerRange(ServerConfiguration[] serverConfigurations)
public async Task InitializeServerRange(ServerConfiguration[] serverConfigurations)
{
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
@@ -173,7 +166,7 @@ public class ServerService : IHostedLifecycleService
await InitializeServer(configuration, existingContainers);
}
private async Task InitializeServer(
public async Task<Server> InitializeServer(
ServerConfiguration serverConfiguration,
IList<ContainerListResponse> existingContainers
)
@@ -190,6 +183,70 @@ public class ServerService : IHostedLifecycleService
lock (Servers)
Servers.Add(server);
return server;
}
public async Task Sync(int serverId)
{
var serverData = await RemoteService.GetServer(serverId);
var serverConfiguration = serverData.ToServerConfiguration();
var server = GetServer(serverId);
if (server == null)
await InitializeServer(serverConfiguration, []);
else
server.UpdateConfiguration(serverConfiguration);
}
public async Task Delete(int serverId)
{
var server = GetServer(serverId);
// If a server with this id doesn't exist we can just exit
if(server == null)
return;
if (server.State == ServerState.Installing)
throw new HttpApiException("Unable to delete a server while it is installing", 400);
#region Callbacks
var deleteCompletion = new TaskCompletionSource();
async Task HandleStateChange(ServerState state)
{
if (state == ServerState.Offline)
await DeleteServer();
}
async Task DeleteServer()
{
await server.CancelTasks();
await server.RemoveInstallationVolume();
await server.RemoveRuntimeVolume();
deleteCompletion.SetResult();
lock (Servers)
Servers.Remove(server);
}
#endregion
// If the server is still online, we are killing it and then
// waiting for the callback to trigger notifying us that the server is now offline
// so we can delete it. The request will pause until then using the deleteCompletion task
if (server.State != ServerState.Offline)
{
server.OnStateChanged += HandleStateChange;
await server.Kill();
await deleteCompletion.Task;
}
else
await DeleteServer();
}
public Server? GetServer(int id)

View File

@@ -1,5 +1,8 @@
using System.Text;
using System.Text.Json;
using Docker.DotNet;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Configuration;
using MoonCore.EnvConfiguration;
using MoonCore.Extended.Extensions;
@@ -41,6 +44,7 @@ public class Startup
await RegisterAppConfiguration();
await RegisterLogging();
await RegisterBase();
await RegisterAuth();
await RegisterDocker();
await RegisterServers();
await RegisterSignalR();
@@ -49,6 +53,7 @@ public class Startup
await BuildWebApplication();
await UseBase();
await UseAuth();
await UseCors();
await UseBaseMiddleware();
@@ -289,4 +294,40 @@ public class Startup
}
#endregion
#region Authentication
private Task RegisterAuth()
{
WebApplicationBuilder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
Configuration.Security.Token
)),
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuer = false,
ClockSkew = TimeSpan.Zero
};
});
WebApplicationBuilder.Services.AddAuthorization();
return Task.CompletedTask;
}
private Task UseAuth()
{
WebApplication.UseAuthentication();
WebApplication.UseAuthorization();
return Task.CompletedTask;
}
#endregion
}

View File

@@ -0,0 +1,84 @@
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.Shared.Http.Responses.Users.Servers;
namespace MoonlightServers.Frontend.Services;
[Scoped]
public class ServerService
{
private readonly HttpApiClient HttpApiClient;
public ServerService(HttpApiClient httpApiClient)
{
HttpApiClient = httpApiClient;
}
public async Task<PagedData<ServerDetailResponse>> GetServers(int page, int perPage)
{
return await HttpApiClient.GetJson<PagedData<ServerDetailResponse>>(
$"api/client/servers?page={page}&pageSize={perPage}"
);
}
public async Task<ServerDetailResponse> GetServer(int serverId)
{
return await HttpApiClient.GetJson<ServerDetailResponse>(
$"api/client/servers/{serverId}"
);
}
public async Task<ServerStatusResponse> GetStatus(int serverId)
{
return await HttpApiClient.GetJson<ServerStatusResponse>(
$"api/client/servers/{serverId}/status"
);
}
public async Task<ServerLogsResponse> GetLogs(int serverId)
{
return await HttpApiClient.GetJson<ServerLogsResponse>(
$"api/client/servers/{serverId}/logs"
);
}
public async Task<ServerWebSocketResponse> GetWebSocket(int serverId)
{
return await HttpApiClient.GetJson<ServerWebSocketResponse>(
$"api/client/servers/{serverId}/ws"
);
}
public async Task Install(int serverId)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/install"
);
}
#region Power actions
public async Task Start(int serverId)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/start"
);
}
public async Task Stop(int serverId)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/stop"
);
}
public async Task Kill(int serverId)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/kill"
);
}
#endregion
}

View File

@@ -1,8 +1,8 @@
@using MoonCore.Helpers
@using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Enums
@using MoonlightServers.Shared.Http.Responses.Users.Servers
@inject HttpApiClient ApiClient
@inject ServerService ServerService
@inject ILogger<ServerCard> Logger
@{
@@ -158,9 +158,7 @@
try
{
Status = await ApiClient.GetJson<ServerStatusResponse>(
$"api/client/servers/{Server.Id}/status"
);
Status = await ServerService.GetStatus(Server.Id);
}
catch (Exception e)
{

View File

@@ -1,11 +1,12 @@
@using MoonCore.Blazor.Tailwind.Alerts
@using MoonCore.Helpers
@using MoonCore.Blazor.Tailwind.Components
@using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Enums
@inherits BaseServerTab
@inject HttpApiClient HttpApiClient
@inject ServerService ServerService
@inject AlertService AlertService
<div class="grid grid-cols-1 md:col-span-2 lg:grid-cols-3">
@@ -34,7 +35,7 @@
await AlertService.ConfirmDanger(
"Server installation",
"Do you really want to reinstall the server? This can potentially lead to loss of data",
() => HttpApiClient.Post($"api/client/servers/{Server.Id}/install")
() => ServerService.Install(Server.Id)
);
}
}

View File

@@ -1,12 +1,12 @@
@page "/servers"
@using MoonCore.Helpers
@using MoonlightServers.Frontend.UI.Components.Servers
@using MoonCore.Blazor.Tailwind.Components
@using MoonCore.Models
@using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Http.Responses.Users.Servers
@inject HttpApiClient ApiClient
@inject ServerService ServerService
<LazyLoader Load="Load">
<div class="flex flex-col gap-y-5">
@@ -42,9 +42,7 @@
private async Task Load(LazyLoader lazyLoader)
{
Servers = await PagedData<ServerDetailResponse>.All(async (page, pageSize) =>
await ApiClient.GetJson<PagedData<ServerDetailResponse>>(
$"api/client/servers?page={page}&pageSize={pageSize}"
)
await ServerService.GetServers(page, pageSize)
);
}
}

View File

@@ -7,11 +7,12 @@
@using MoonCore.Helpers
@using MoonlightServers.Frontend.Interfaces
@using MoonlightServers.Frontend.Models
@using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Enums
@using MoonlightServers.Frontend.UI.Components
@using MoonlightServers.Frontend.UI.Components.Servers.ServerTabs
@inject HttpApiClient ApiClient
@inject ServerService ServerService
@inject IEnumerable<IServerTabProvider> TabProviders
@implements IAsyncDisposable
@@ -164,25 +165,19 @@
try
{
// Load meta data
Server = await ApiClient.GetJson<ServerDetailResponse>(
$"api/client/servers/{ServerId}"
);
Server = await ServerService.GetServer(ServerId);
// Load server tabs
foreach (var serverTabProvider in TabProviders)
Tabs.AddRange(await serverTabProvider.GetTabs(Server));
// Load initial status for first render
var status = await ApiClient.GetJson<ServerStatusResponse>(
$"api/client/servers/{ServerId}/status"
);
var status = await ServerService.GetStatus(ServerId);
State = status.State;
// Load initial messages
var initialLogs = await ApiClient.GetJson<ServerLogsResponse>(
$"api/client/servers/{ServerId}/logs"
);
var initialLogs = await ServerService.GetLogs(ServerId);
InitialConsoleMessage = "";
@@ -190,9 +185,7 @@
InitialConsoleMessage += message;
// Load websocket meta
var websocketDetails = await ApiClient.GetJson<ServerWebSocketResponse>(
$"api/client/servers/{ServerId}/ws"
);
var websocketDetails = await ServerService.GetWebSocket(ServerId);
// Build signal r
HubConnection = new HubConnectionBuilder()
@@ -232,13 +225,13 @@
}
private async Task Start()
=> await ApiClient.Post($"api/client/servers/{Server.Id}/start");
=> await ServerService.Start(ServerId);
private async Task Stop()
=> await ApiClient.Post($"api/client/servers/{Server.Id}/stop");
=> await ServerService.Stop(ServerId);
private async Task Kill()
=> await ApiClient.Post($"api/client/servers/{Server.Id}/kill");
=> await ServerService.Kill(ServerId);
public async ValueTask DisposeAsync()
{

View File

@@ -25,7 +25,7 @@ public class CreateServerRequest
public string? StartupOverride { get; set; }
public int DockerImageIndex { get; set; }
public int DockerImageIndex { get; set; } = -1;
public int StarId { get; set; }