Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
- name: Build
run: dotnet build --configuration Release --no-restore

- name: Test
run: dotnet test --no-restore --verbosity normal

- name: Publish ApiService
run: dotnet publish ./src/DistributedCodingCompetition.ApiService -c Release --os linux --arch x64 /t:PublishContainer -o ./publish/apiservice

Expand Down
7 changes: 7 additions & 0 deletions DistributedCodingCompetition.sln
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{BD82C632-3
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PistonSimulator", "tests\PistonSimulator\PistonSimulator.csproj", "{908D38BC-719B-46A7-B5F9-90D93092D702}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DistributedCodingCompetition.CodeExecutionUnitTests", "tests\DistributedCodingCompetition.CodeExecutionUnitTests\DistributedCodingCompetition.CodeExecutionUnitTests.fsproj", "{8513840D-A167-472B-B8D2-32DD668DE20A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -177,6 +179,10 @@ Global
{908D38BC-719B-46A7-B5F9-90D93092D702}.Debug|Any CPU.Build.0 = Debug|Any CPU
{908D38BC-719B-46A7-B5F9-90D93092D702}.Release|Any CPU.ActiveCfg = Release|Any CPU
{908D38BC-719B-46A7-B5F9-90D93092D702}.Release|Any CPU.Build.0 = Release|Any CPU
{8513840D-A167-472B-B8D2-32DD668DE20A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8513840D-A167-472B-B8D2-32DD668DE20A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8513840D-A167-472B-B8D2-32DD668DE20A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8513840D-A167-472B-B8D2-32DD668DE20A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -214,6 +220,7 @@ Global
{E3356F27-B7FA-4792-AB72-5B31BAD23491} = {A521F175-205A-407B-964C-04F247C4C88C}
{4FA6BD6C-3F67-457C-9671-9C04F82346F9} = {A521F175-205A-407B-964C-04F247C4C88C}
{908D38BC-719B-46A7-B5F9-90D93092D702} = {BD82C632-3052-4A82-A507-55933DA1C512}
{8513840D-A167-472B-B8D2-32DD668DE20A} = {BD82C632-3052-4A82-A507-55933DA1C512}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D27D9E1A-CBE2-47C5-9C7F-18D5154A0902}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System.Text.Json;

namespace DistributedCodingCompetition.ApiService.Client;

/// <summary>
/// A client for interacting with the DistributedCodingCompetition API.
/// </summary>
/// <typeparam name="TOwner">Owner Service</typeparam>
/// <param name="httpClient">httpclient to use</param>
/// <param name="httpClient">http client to use</param>
/// <param name="logger">logger</param>
/// <param name="prefix">url prefix</param>
internal class ApiClient<TOwner>(HttpClient httpClient, ILogger<TOwner> logger, string prefix)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
global using System.Text.Json;
global using System.Net.Http.Json;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.DependencyInjection;
global using DistributedCodingCompetition.ApiService.Models;

namespace DistributedCodingCompetition.ApiService.Client;

/// <summary>
/// Extension methods for dependency injection.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public Task<bool> TryDeleteUserAsync(Guid id) =>

/// <inheritdoc/>
public Task<(bool, UserResponseDTO?)> TryReadUserByEmailAsync(string email) =>
apiClient.GetAsync<UserResponseDTO>($"/email/{email}");
apiClient.GetAsync<UserResponseDTO>($"/email/{Uri.EscapeDataString(email)}");

/// <inheritdoc/>
public Task<(bool, UserResponseDTO?)> TryReadUserByUsernameAsync(string username) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ namespace DistributedCodingCompetition.ApiService.MigrationService;
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="hostApplicationLifetime"></param>
public sealed class Worker(IServiceProvider serviceProvider, IHostApplicationLifetime hostApplicationLifetime, IConfiguration configuration) : BackgroundService
/// <param name="configuration"></param>
public sealed class Worker(IServiceProvider serviceProvider, IHostApplicationLifetime hostApplicationLifetime, IConfiguration configuration, ILogger<Worker> logger) : BackgroundService
{
public const string ActivitySourceName = "API Migrations";

Expand All @@ -22,7 +23,6 @@ public sealed class Worker(IServiceProvider serviceProvider, IHostApplicationLif
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
using var activity = s_activitySource.StartActivity("Migrating database", ActivityKind.Client);

try
{
using var scope = serviceProvider.CreateScope();
Expand All @@ -31,16 +31,16 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
await EnsureDatabaseAsync(dbContext, cancellationToken);
await RunMigrationAsync(dbContext, cancellationToken);

if (configuration["Seed"] is "true")
if (Convert.ToBoolean(configuration["Seed"]))
await SeedDataAsync(dbContext, cancellationToken);
}
catch (Exception ex)
{
activity?.RecordException(ex);
throw;
}

hostApplicationLifetime.StopApplication();
if (Convert.ToBoolean(configuration["ExitAfterMigration"]))
hostApplicationLifetime.StopApplication();
}

private static async Task EnsureDatabaseAsync(ContestContext dbContext, CancellationToken cancellationToken)
Expand Down Expand Up @@ -69,8 +69,9 @@ await strategy.ExecuteAsync(async () =>
});
}

private static async Task SeedDataAsync(ContestContext dbContext, CancellationToken cancellationToken)
private async Task SeedDataAsync(ContestContext dbContext, CancellationToken cancellationToken)
{
logger.LogInformation("Seeding data");
User user1 = new()
{
Id = Guid.Parse("134904d0-9515-4ceb-84d0-2cae5bf60f9d"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Seed": true
"Seed": true,
"ExitAfterMigration": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Seed": false
"Seed": false,
"ExitAfterMigration": true
}
3 changes: 2 additions & 1 deletion src/DistributedCodingCompetition.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

var mongo = builder.AddMongoDB("mongo");

var executorDatabase = postgres.AddDatabase("evaluationdb");
var executorDatabase = mongo.AddDatabase("evaluationdb");

var contestDatabase = postgres.AddDatabase("contestdb");

Expand All @@ -22,6 +22,7 @@

var codeExecution = builder.AddProject<Projects.DistributedCodingCompetition_CodeExecution>("codeexecution")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WithReference(executorDatabase);

var apiService = builder.AddProject<Projects.DistributedCodingCompetition_ApiService>("apiservice")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public static IServiceCollection AddDistributedCodingCompetitionCodeExecution(th
{
serviceDescriptors.AddSingleton<ICodeExecutionService, CodeExecutionService>();
serviceDescriptors.AddHttpClient<ICodeExecutionService, CodeExecutionService>(client => client.BaseAddress = apiAddress);

serviceDescriptors.AddSingleton<IExecutionManagementService, ExecutionManagementService>();
serviceDescriptors.AddHttpClient<IExecutionManagementService, ExecutionManagementService>(client => client.BaseAddress = apiAddress);
return serviceDescriptors;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace DistributedCodingCompetition.CodeExecution.Client;

/// <inheritdoc/>
public class ExecutionManagementService(HttpClient httpClient, ILogger<ExecutionManagementService> logger) : IExecutionManagementService
{
/// <inheritdoc/>
public async Task<ExecRunnerResponseDTO> CreateExecRunnerAsync(ExecRunnerRequestDTO request)
{
logger.LogInformation("Creating ExecRunner {@ExecRunner}", request);
var response = await httpClient.PostAsJsonAsync("management/runners", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ExecRunnerResponseDTO>() ?? throw new Exception("Failed to parse response");
}

/// <inheritdoc/>
public async Task<ExecRunnerResponseDTO> ReadExecRunnerAsync(Guid id)
{
logger.LogInformation("Reading ExecRunner {@Id}", id);
var response = await httpClient.GetAsync($"management/runners/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ExecRunnerResponseDTO>() ?? throw new Exception("Failed to parse response");
}

/// <inheritdoc/>
public async Task UpdateExecRunnerAsync(Guid id, ExecRunnerRequestDTO request)
{
logger.LogInformation("Updating ExecRunner {@Id} with {@ExecRunner}", id, request);
var response = await httpClient.PutAsJsonAsync($"management/runners/{id}", request);
response.EnsureSuccessStatusCode();
}

/// <inheritdoc/>
public async Task DeleteExecRunnerAsync(Guid id)
{
logger.LogInformation("Deleting ExecRunner {@Id}", id);
var response = await httpClient.DeleteAsync($"management/runners/{id}");
response.EnsureSuccessStatusCode();
}

/// <inheritdoc/>
public async Task<IReadOnlyList<ExecRunnerResponseDTO>> ListExecRunnersAsync()
{
logger.LogInformation("Listing ExecRunners");
var response = await httpClient.GetAsync("management/runners");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<IReadOnlyList<ExecRunnerResponseDTO>>() ?? throw new Exception("Failed to parse response");
}

/// <inheritdoc/>
public async Task SetPackagesAsync(Guid id, IEnumerable<string> packages)
{
logger.LogInformation("Setting packages for ExecRunner {@Id} with {@Packages}", id, packages);
var response = await httpClient.PostAsJsonAsync($"management/runners/{id}/packages", packages);
response.EnsureSuccessStatusCode();
}

/// <inheritdoc/>
public async Task<IEnumerable<string>> InstalledPackagesAsync(Guid id)
{
logger.LogInformation("Getting installed packages for ExecRunner {@Id}", id);
var response = await httpClient.GetAsync($"management/runners/{id}/packages/installed");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<IEnumerable<string>>() ?? throw new Exception("Failed to parse response");
}

/// <inheritdoc/>
public async Task<IEnumerable<string>> AvailablePackagesAsync(Guid id)
{
logger.LogInformation("Getting available packages for ExecRunner {@Id}", id);
var response = await httpClient.GetAsync($"management/runners/{id}/packages/available");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<IEnumerable<string>>() ?? throw new Exception("Failed to parse response");
}

/// <inheritdoc/>
public async Task<IEnumerable<string>> InstalledLanguagesAsync(Guid id)
{
logger.LogInformation("Getting installed languages for ExecRunner {@Id}", id);
var response = await httpClient.GetAsync($"management/runners/{id}/languages");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<IEnumerable<string>>() ?? throw new Exception("Failed to parse response");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace DistributedCodingCompetition.CodeExecution.Client;

using DistributedCodingCompetition.ExecutionShared;

/// <summary>
/// Service for managing exec runners.
/// </summary>
public interface IExecutionManagementService
{
/// <summary>
/// Create a new exec runner
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
Task<ExecRunnerResponseDTO> CreateExecRunnerAsync(ExecRunnerRequestDTO request);

/// <summary>
/// Read an exec runner
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<ExecRunnerResponseDTO> ReadExecRunnerAsync(Guid id);

/// <summary>
/// Update an exec runner
/// </summary>
/// <param name="id"></param>
/// <param name="request"></param>
/// <returns></returns>
Task UpdateExecRunnerAsync(Guid id, ExecRunnerRequestDTO request);

/// <summary>
/// Delete an exec runner
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task DeleteExecRunnerAsync(Guid id);

/// <summary>
/// List all exec runners
/// </summary>
/// <returns></returns>
Task<IReadOnlyList<ExecRunnerResponseDTO>> ListExecRunnersAsync();

/// <summary>
/// Set the packages of an exec runner.
///
/// While this returns immediately, the installation process continues behind the scenes on the exec runner.
/// Make sure to check the installed packages to see when the installation is complete.
/// </summary>
/// <param name="id"></param>
/// <param name="packages"></param>
/// <returns></returns>
Task SetPackagesAsync(Guid id, IEnumerable<string> packages);

/// <summary>
/// Get the installed packages of an exec runner.
/// </summary>
/// <param name="id"></param>
/// <returns>list of packages, or an empty list if installation in progress</returns>
Task<IEnumerable<string>> InstalledPackagesAsync(Guid id);

/// <summary>
/// Get the available packages of an exec runner.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<IEnumerable<string>> AvailablePackagesAsync(Guid id);

/// <summary>
/// Get the installed languages of an exec runner.
/// </summary>
/// <returns></returns>
Task<IEnumerable<string>> InstalledLanguagesAsync(Guid id);
}
Loading