Repo controller create

This commit is contained in:
Jordan Brown
2018-07-29 22:02:04 -04:00
parent 4418fc0964
commit 0a585c8cb4
11 changed files with 218 additions and 48 deletions
@@ -9,6 +9,18 @@ namespace Tgstation.Server.Api.Models.Internal
[Model(RightsType.Repository, ReadRight = RepositoryRights.Read, RequiresInstance = true)]
public class RepositorySettings
{
/// <summary>
/// The origin URL. If <see langword="null"/>, the <see cref="Repository"/> does not exist
/// </summary>
[Permissions(WriteRight = RepositoryRights.SetOrigin)]
public string Origin { get; set; }
/// <summary>
/// The last commit recognized from <see cref="Origin"/>
/// </summary>
[Permissions(DenyWrite = true)]
public string LastOriginCommitSha { get; set; }
/// <summary>
/// The name of the committer
/// </summary>
@@ -8,17 +8,11 @@ namespace Tgstation.Server.Api.Models
/// </summary>
public sealed class Repository : Internal.RepositorySettings
{
/// <summary>
/// The origin URL. If <see langword="null"/>, the <see cref="Repository"/> does not exist
/// </summary>
[Permissions(WriteRight = RepositoryRights.SetOrigin)]
public string Origin { get; set; }
/// <summary>
/// The commit HEAD points to
/// </summary>
[Permissions(WriteRight = RepositoryRights.SetSha)]
public string NewRevision { get; set; }
public string Sha { get; set; }
/// <summary>
/// The current <see cref="Models.RevisionInformation"/> for the <see cref="Repository"/>
@@ -4,10 +4,15 @@ namespace Tgstation.Server.Api.Models
{
/// <inheritdoc />
public sealed class RevisionInformation : Internal.RevisionInformation
{
{
/// <summary>
/// The <see cref="TestMerge"/>s active in the <see cref="RevisionInformation"/>
/// </summary>
public List<TestMerge> TestMerges { get; set; }
/// <summary>
/// The <see cref="CompileJob"/>s made from the <see cref="RevisionInformation"/>
/// </summary>
public List<CompileJob> CompileJobs { get; set; }
}
}
@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading;
@@ -33,7 +34,7 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public StaticFiles.IConfiguration Configuration { get; }
/// <summary>
/// The <see cref="ICompileJobConsumer"/> for the <see cref="Instance"/>
/// </summary>
@@ -49,6 +50,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IDmbFactory dmbFactory;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Instance"/>
/// </summary>
readonly ILogger<Instance> logger;
/// <summary>
/// The <see cref="Api.Models.Instance"/> for the <see cref="Instance"/>
/// </summary>
@@ -76,7 +82,8 @@ namespace Tgstation.Server.Host.Components
/// <param name="compileJobConsumer">The value of <see cref="compileJobConsumer"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
/// <param name="dmbFactory">The value of <see cref="dmbFactory"/></param>
public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory)
/// <param name="logger">The value of <see cref="logger"/></param>
public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, ILogger<Instance> logger)
{
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
@@ -88,6 +95,7 @@ namespace Tgstation.Server.Host.Components
this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
@@ -109,9 +117,8 @@ namespace Tgstation.Server.Host.Components
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task TimerLoop(int minutes, CancellationToken cancellationToken)
{
try
{
while (true)
while (true)
try
{
await Task.Delay(new TimeSpan(0, minutes, 0), cancellationToken).ConfigureAwait(false);
@@ -128,14 +135,31 @@ namespace Tgstation.Server.Host.Components
});
using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
await dbTask.ConfigureAwait(false);
try
{
await dbTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception e)
{
logger.LogWarning("Database error in auto update loop! Exception: {0}", e);
continue;
}
if (repo == null)
continue;
await repo.FetchOrigin(accessToken, cancellationToken).ConfigureAwait(false);
await repo.ResetToOrigin(cancellationToken).ConfigureAwait(false);
var job = await DreamMaker.Compile(projectName, timeout, repo, cancellationToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException) { }
catch (OperationCanceledException) { }
catch (Exception e)
{
logger.LogError("Error in auto update loop! Exception: {0}", e);
}
}
/// <inheritdoc />
@@ -143,7 +143,7 @@ namespace Tgstation.Server.Host.Components
var commandFactory = new CommandFactory(application);
var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory, providerFactory);
var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager);
var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer);
try
{
var byond = new ByondManager(byondIOManager, byondInstaller, loggerFactory.CreateLogger<ByondManager>());
@@ -160,7 +160,7 @@ namespace Tgstation.Server.Host.Components
{
var dreamMaker = new DreamMaker(byond, ioManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, loggerFactory.CreateLogger<DreamMaker>());
return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory);
return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, loggerFactory.CreateLogger<Instance>());
}
catch
{
@@ -13,16 +13,24 @@ namespace Tgstation.Server.Host.Components.Repository
/// Attempt to load the <see cref="IRepository"/> from the default location
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>The loaded <see cref="IRepository"/></returns>
/// <returns>The loaded <see cref="IRepository"/> if it exists, <see langword="null"/> otherwise</returns>
Task<IRepository> LoadRepository(CancellationToken cancellationToken);
/// <summary>
/// Delete the current <see cref="IRepository"/> and replaces it with a clone of the repository at <paramref name="url"/>
/// Clone the repository at <paramref name="url"/>
/// </summary>
/// <param name="url">The <see cref="Uri"/> of the remote repository to clone</param>
/// <param name="initialBranch">The branch to clone</param>
/// <param name="accessString">The access string to clone from <paramref name="url"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>The newly cloned <see cref="IRepository"/></returns>
Task<IRepository> CloneRepository(Uri url, string accessString, CancellationToken cancellationToken);
Task<IRepository> CloneRepository(Uri url, string initialBranch, string accessString, CancellationToken cancellationToken);
/// <summary>
/// Delete the current repository
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task DeleteRepository(CancellationToken cancellationToken);
}
}
@@ -3,6 +3,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Repository
@@ -15,6 +16,11 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="IEventConsumer"/> for the <see cref="RepositoryManager"/>
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="RepositorySettings"/> for the <see cref="RepositoryManager"/>
/// </summary>
@@ -30,10 +36,12 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
/// <param name="repositorySettings">The value of <see cref="repositorySettings"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager)
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager, IEventConsumer eventConsumer)
{
this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
semaphore = new SemaphoreSlim(1);
}
@@ -41,28 +49,32 @@ namespace Tgstation.Server.Host.Components.Repository
public void Dispose() => semaphore.Dispose();
/// <inheritdoc />
public async Task<IRepository> CloneRepository(Uri url, string accessString, CancellationToken cancellationToken)
public async Task<IRepository> CloneRepository(Uri url, string initialBranch, string accessString, CancellationToken cancellationToken)
{
await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false);
await Task.Factory.StartNew(() =>
{
string path = null;
try
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
{
path = LibGit2Sharp.Repository.Clone(Repository.GenerateAuthUrl(url.ToString(), accessString), ioManager.ResolvePath("."), new CloneOptions
{
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
RecurseSubmodules = true,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested
});
}
catch (UserCancelledException) { }
cancellationToken.ThrowIfCancellationRequested();
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
await DeleteRepository(cancellationToken).ConfigureAwait(false);
await Task.Factory.StartNew(() =>
{
string path = null;
try
{
path = LibGit2Sharp.Repository.Clone(Repository.GenerateAuthUrl(url.ToString(), accessString), ioManager.ResolvePath("."), new CloneOptions
{
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
RecurseSubmodules = true,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
BranchName = initialBranch
});
}
catch (UserCancelledException) { }
cancellationToken.ThrowIfCancellationRequested();
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
}
return await LoadRepository(cancellationToken).ConfigureAwait(false);
}
@@ -73,14 +85,27 @@ namespace Tgstation.Server.Host.Components.Repository
LibGit2Sharp.Repository repo = null;
await Task.Factory.StartNew(() =>
{
repo = new LibGit2Sharp.Repository(ioManager.ResolvePath("."));
try
{
repo = new LibGit2Sharp.Repository(ioManager.ResolvePath("."));
}
catch (RepositoryNotFoundException) { }
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
if (repo == null)
return null;
var localSemaphore = semaphore;
return new Repository(repo, ioManager, () =>
return new Repository(repo, ioManager, eventConsumer, () =>
{
localSemaphore?.Release();
localSemaphore = null;
});
}
/// <inheritdoc />
public async Task DeleteRepository(CancellationToken cancellationToken)
{
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false);
}
}
}
@@ -129,6 +129,11 @@ namespace Tgstation.Server.Host.Controllers
Task<RevisionInformation> revInfoTask;
using (var repo = await instance.RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo == null)
{
job.ExceptionDetails = "Missing repository!";
return;
}
revInfoTask = databaseContext.RevisionInformations.Where(x => x.CommitSha == repo.Head).Select(x => new RevisionInformation { Id = x.Id }).FirstAsync();
compileJob = await instance.DreamMaker.Compile(projectName, timeout.Value, repo, cancellationToken).ConfigureAwait(false);
}
@@ -1,5 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
@@ -9,8 +17,8 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Controller for managing the <see cref="Api.Models.Repository"/>s
/// </summary>
[Route("/" + nameof(Api.Models.Repository))]
public sealed class RepositoryController : ModelController<Api.Models.Repository>
[Route("/" + nameof(Repository))]
public sealed class RepositoryController : ModelController<Repository>
{
/// <summary>
/// The <see cref="IInstanceManager"/> for the <see cref="RepositoryController"/>
@@ -22,10 +30,83 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="instanceMangaer">The value of <see cref="instanceMangaer"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
}
static string GetAccessString(Api.Models.Internal.RepositorySettings repositorySettings) => repositorySettings.AccessUser != null ? String.Concat(repositorySettings.AccessUser, '@', repositorySettings.AccessToken) : null;
async Task<bool> PopulateApi(Repository model, Components.Repository.IRepository repository, string lastOriginCommitSha, CancellationToken cancellationToken)
{
model.IsGitHub = repository.IsGitHubRepository;
model.Origin = repository.Origin;
model.Reference = repository.Reference;
model.Sha = repository.Head;
//rev info stuff
var revisionInfo = await DatabaseContext.RevisionInformations.Where(x => x.CommitSha == model.Sha)
.Include(x => x.CompileJobs)
.Include(x => x.TestMerges) //minimal info, they can query the rest if they're allowed
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); //search every rev info because LOL SHA COLLISIONS
var needsDbUpdate = revisionInfo == default;
if (needsDbUpdate)
{
//needs insertion
revisionInfo = new Models.RevisionInformation
{
CommitSha = model.Sha,
CompileJobs = new List<Models.CompileJob>(),
TestMerges = new List<Models.TestMerge>(), //non null vals for api returns
OriginCommitSha = lastOriginCommitSha ?? model.Sha
};
DatabaseContext.RevisionInformations.Add(revisionInfo);
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
}
model.RevisionInformation = revisionInfo.ToApi();
return needsDbUpdate;
}
/// <inheritdoc />
[TgsAuthorize(RepositoryRights.SetOrigin)]
public override async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
{
if (model == null)
return BadRequest(new { message = "Missing request model!" });
if (model.Origin == null)
return BadRequest(new { message = "Missing repo origin!" });
if (model.AccessUser == null ^ model.AccessToken == null)
return BadRequest(new { message = "Either both accessToken and accessUser must be present or neither!" });
var currentModel = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.RepositorySettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (currentModel == default)
return StatusCode((int)HttpStatusCode.Gone);
currentModel.AccessToken = model.AccessToken;
currentModel.AccessUser = model.AccessUser;
currentModel.Origin = model.Origin; //intentionally only these fields, user not allowed to change anything else atm
var cloneBranch = model.Reference;
var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
using (var repo = await repoManager.CloneRepository(new Uri(currentModel.Origin), cloneBranch, GetAccessString(currentModel), cancellationToken).ConfigureAwait(false))
{
if (repo == null)
//clone conflict
return Conflict();
var api = currentModel.ToApi();
await PopulateApi(api, repo, null, cancellationToken).ConfigureAwait(false);
currentModel.LastOriginCommitSha = repo.Head;
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
return Json(api);
}
}
}
}
@@ -1,9 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings
public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings, IApiConvertable<Repository>
{
/// <summary>
/// The row Id
@@ -20,5 +21,19 @@ namespace Tgstation.Server.Host.Models
/// </summary>
[Required]
public Instance Instance { get; set; }
/// <inheritdoc />
public Repository ToApi() => new Repository {
AccessToken = AccessToken,
AccessUser = AccessUser,
AutoUpdatesKeepTestMerges = AutoUpdatesKeepTestMerges,
AutoUpdatesSynchronize = AutoUpdatesSynchronize,
CommitterEmail = CommitterEmail,
CommitterName = CommitterName,
//intentionally don't populate origin just in case
PushTestMergeCommits = PushTestMergeCommits,
//revision information and the rest retrieved by controller
ShowTestMergeCommitters = ShowTestMergeCommitters
};
}
}
@@ -31,7 +31,8 @@ namespace Tgstation.Server.Host.Models
{
CommitSha = CommitSha,
OriginCommitSha = OriginCommitSha,
TestMerges = TestMerges.Select(x => x.ToApi()).ToList()
TestMerges = TestMerges.Select(x => x.ToApi()).ToList(),
CompileJobs = CompileJobs.Select(x => x.ToApi()).ToList()
};
}
}