ByondController

This commit is contained in:
Jordan Brown
2018-07-30 09:19:45 -04:00
parent bfaa4b81d3
commit ef1ae99fab
12 changed files with 282 additions and 37 deletions
+6
View File
@@ -14,5 +14,11 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Permissions(ReadRight = ByondRights.ReadActive, WriteRight = ByondRights.ChangeVersion)]
public Version Version { get; set; }
/// <summary>
/// The <see cref="Job"/> being used to install a new <see cref="Version"/>
/// </summary>
[Permissions(DenyWrite = true)]
public Job InstallJob { get; set; }
}
}
@@ -32,6 +32,12 @@ namespace Tgstation.Server.Api.Models
[Permissions(DenyWrite = true)]
public bool? IsGitHub { get; set; }
/// <summary>
/// The <see cref="Job"/> started by the <see cref="Repository"/> if any
/// </summary>
[Permissions(DenyWrite = true)]
public Job ActiveJob { get; set; }
/// <summary>
/// Do the equivalent of a git pull. Will attempt to merge unless <see cref="Reference"/> is also specified
/// </summary>
+10 -2
View File
@@ -17,8 +17,16 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
ReadActive = 1,
/// <summary>
/// User may change to any BYOND version
/// User may list all installed BYOND versions
/// </summary>
ChangeVersion = 4
ListInstalled = 2,
/// <summary>
/// User may change the active BYOND version
/// </summary>
ChangeVersion = 4,
/// <summary>
/// User may cancel version installations
/// </summary>
CancelInstall = 8
}
}
@@ -55,6 +55,14 @@ namespace Tgstation.Server.Api.Rights
/// <summary>
/// User may delete the <see cref="Models.Repository"/>
/// </summary>
Delete = 2048
Delete = 2048,
/// <summary>
/// User may cancel clone operations
/// </summary>
CancelClone = 4096,
/// <summary>
/// User may cancel synchronize operations
/// </summary>
CancelSynchronize = 8192
}
}
@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Byond
@@ -20,6 +21,16 @@ namespace Tgstation.Server.Host.Components.Byond
/// <inheritdoc />
public Version ActiveVersion { get; private set; }
/// <inheritdoc />
public IReadOnlyList<Version> InstalledVersions
{
get
{
lock (installedVersions)
return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
}
}
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="ByondManager"/>
/// </summary>
@@ -40,6 +51,13 @@ namespace Tgstation.Server.Host.Components.Byond
/// </summary>
readonly Dictionary<string, Task> installedVersions;
/// <summary>
/// The <see cref="SemaphoreSlim"/> for the <see cref="ByondManager"/>
/// </summary>
readonly SemaphoreSlim semaphore;
static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
/// <summary>
/// Construct a <see cref="ByondManager"/>
/// </summary>
@@ -53,9 +71,11 @@ namespace Tgstation.Server.Host.Components.Byond
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
installedVersions = new Dictionary<string, Task>();
semaphore = new SemaphoreSlim(1);
}
static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
/// <inheritdoc />
public void Dispose() => semaphore.Dispose();
async Task InstallVersion(Version version, CancellationToken cancellationToken)
{
@@ -95,8 +115,11 @@ namespace Tgstation.Server.Host.Components.Byond
public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
{
await InstallVersion(version, cancellationToken).ConfigureAwait(false);
await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
ActiveVersion = version;
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
{
await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
ActiveVersion = version;
}
}
/// <inheritdoc />
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +9,18 @@ namespace Tgstation.Server.Host.Components.Byond
/// <summary>
/// For managing the BYOND installation
/// </summary>
public interface IByondManager : IHostedService
public interface IByondManager : IHostedService, IDisposable
{
/// <summary>
/// The currently active BYOND version
/// </summary>
Version ActiveVersion { get; }
/// <summary>
/// The installed BYOND versions
/// </summary>
IReadOnlyList<Version> InstalledVersions { get; }
/// <summary>
/// Change the active BYOND version
/// </summary>
@@ -12,6 +12,11 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
sealed class Repository : IRepository
{
/// <summary>
/// Indication of a GitHub repository
/// </summary>
public const string GitHubUrl = "://github.com/";
const string UnknownReference = "<UNKNOWN>";
/// <summary>
@@ -86,7 +91,7 @@ namespace Tgstation.Server.Host.Components.Repository
this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
IsGitHubRepository = Origin.ToUpperInvariant().Contains("://GITHUB.COM/");
IsGitHubRepository = Origin.ToUpperInvariant().Contains(GitHubUrl.ToUpperInvariant());
if (IsGitHubRepository)
{
GetRepositoryOwnerName(Origin, out var owner, out var name);
@@ -53,28 +53,38 @@ namespace Tgstation.Server.Host.Components.Repository
{
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
{
await DeleteRepository(cancellationToken).ConfigureAwait(false);
await Task.Factory.StartNew(() =>
try
{
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);
}
catch
{
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
});
await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
}
catch (UserCancelledException) { }
cancellationToken.ThrowIfCancellationRequested();
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
}
catch { }
throw;
}
return await LoadRepository(cancellationToken).ConfigureAwait(false);
}
@@ -0,0 +1,108 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
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.Core;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// Controller for managing <see cref="Api.Models.Byond.Version"/>s
/// </summary>
[Route("/" + nameof(Byond))]
public sealed class ByondController : ModelController<Api.Models.Byond>
{
/// <summary>
/// The <see cref="IInstanceManager"/> for the <see cref="ByondController"/>
/// </summary>
readonly IInstanceManager instanceManager;
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="ByondController"/>
/// </summary>
readonly IJobManager jobManager;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="ByondController"/>
/// </summary>
readonly ILogger<ByondController> logger;
/// <summary>
/// Construct a <see cref="ByondController"/>
/// </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="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger<ByondController> logger) : base(databaseContext, authenticationContextFactory, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
[TgsAuthorize(ByondRights.ReadActive)]
public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)
Json(new Api.Models.Byond
{
Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion
}));
/// <inheritdoc />
[TgsAuthorize(ByondRights.ListInstalled)]
public override Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)
Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
{
Version = x
})));
/// <inheritdoc />
[TgsAuthorize(ByondRights.ChangeVersion)]
public override async Task<IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
{
if (model == null)
return BadRequest(new { message = "Missing request model!" });
if(model.Version == null)
return BadRequest(new { message = "Missing version!" });
var byondManager = instanceManager.GetInstance(Instance).ByondManager;
//remove cruff fields
var installingVersion = new Version(model.Version.Major, model.Version.Major);
var result = new Api.Models.Byond();
if (byondManager.InstalledVersions.Any(x => x == model.Version))
{
logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false);
}
else
{
logger.LogInformation("User ID {0} installing BYOND version to {2} on instance ID {1}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
//run the install through the job manager
var job = new Models.Job
{
StartedBy = AuthenticationContext.User,
CancelRightsType = RightsType.Byond,
CancelRight = (int)ByondRights.CancelInstall,
Instance = Instance
};
await jobManager.RegisterOperation(job, (paramJob, serviceProvicer, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
result.InstallJob = job.ToApi();
}
result.Version = byondManager.ActiveVersion;
return Json(result);
}
}
}
@@ -55,15 +55,16 @@ namespace Tgstation.Server.Host.Controllers
if (instance.Watchdog.Running)
return StatusCode((int)HttpStatusCode.Gone);
await jobManager.RegisterOperation(new Models.Job
var job = new Models.Job
{
Description = "Launch DreamDaemon",
CancelRight = (int)DreamDaemonRights.Shutdown,
CancelRightsType = RightsType.DreamDaemon,
Instance = Instance,
StartedBy = AuthenticationContext.User
},
async (job, serviceProvider, innerCt) =>
};
await jobManager.RegisterOperation(job,
async (paramJob, serviceProvider, innerCt) =>
{
var result = await instance.Watchdog.Launch(innerCt).ConfigureAwait(false);
if (result == null)
@@ -72,7 +73,7 @@ namespace Tgstation.Server.Host.Controllers
throw new Exception("Failed to launch watchdog!");
},
cancellationToken).ConfigureAwait(false);
return Ok();
return Json(job);
}
/// <inheritdoc />
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -38,6 +39,11 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
readonly IJobManager jobManager;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="RepositoryController"/>
/// </summary>
readonly ILogger<RepositoryController> logger;
/// <summary>
/// Construct a <see cref="RepositoryController"/>
/// </summary>
@@ -46,11 +52,13 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="gitHubClient">The value of <see cref="gitHubClient"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, Octokit.IGitHubClient gitHubClient, IJobManager jobManager) : base(databaseContext, authenticationContextFactory, true)
/// <param name="logger">The value of <see cref="logger"/></param>
public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, Octokit.IGitHubClient gitHubClient, IJobManager jobManager, ILogger<RepositoryController> logger) : base(databaseContext, authenticationContextFactory, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
static string GetAccessString(Api.Models.Internal.RepositorySettings repositorySettings) => repositorySettings.AccessUser != null ? String.Concat(repositorySettings.AccessUser, '@', repositorySettings.AccessToken) : null;
@@ -115,6 +123,13 @@ namespace Tgstation.Server.Host.Controllers
if (currentModel == default)
return StatusCode((int)HttpStatusCode.Gone);
//normalize github urls
const string BadGitHubUrl = "://www.github.com/";
var uiOrigin = model.Origin.ToUpperInvariant();
var uiBad = BadGitHubUrl.ToUpperInvariant();
var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant();
if (uiOrigin.Contains(uiBad))
model.Origin = uiOrigin.Replace(uiBad, uiGitHub);
currentModel.AccessToken = model.AccessToken;
currentModel.AccessUser = model.AccessUser; //intentionally only these fields, user not allowed to change anything else atm
@@ -122,15 +137,37 @@ namespace Tgstation.Server.Host.Controllers
var origin = model.Origin;
var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
using (var repo = await repoManager.CloneRepository(new Uri(origin), cloneBranch, GetAccessString(currentModel), cancellationToken).ConfigureAwait(false))
using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo == null)
//clone conflict
return Conflict();
var job = new Models.Job
{
Description = "Clone repository",
StartedBy = AuthenticationContext.User,
CancelRightsType = RightsType.Repository,
CancelRight = (int)RepositoryRights.CancelClone,
Instance = Instance
};
var api = currentModel.ToApi();
await PopulateApi(api, repo, null, null, cancellationToken).ConfigureAwait(false);
currentModel.LastOriginCommitSha = repo.Head;
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, ct) =>
{
using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, GetAccessString(currentModel), cancellationToken).ConfigureAwait(false))
{
if (repos == null)
throw new Exception("Filesystem conflict while cloning repository!");
await PopulateApi(api, repo, null, null, cancellationToken).ConfigureAwait(false);
}
}, cancellationToken).ConfigureAwait(false);
api.Origin = model.Origin;
api.Reference = model.Reference;
api.IsGitHub = model.Origin.ToUpperInvariant().Contains(uiGitHub);
api.ActiveJob = job.ToApi();
return Json(api);
}
}
@@ -154,7 +191,17 @@ namespace Tgstation.Server.Host.Controllers
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
await instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken).ConfigureAwait(false);
logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id);
var job = new Models.Job
{
Description = "Delete repository",
StartedBy = AuthenticationContext.User,
Instance = Instance
};
var api = currentModel.ToApi();
await jobManager.RegisterOperation(job, (paramJob, serviceProvider, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
api.ActiveJob = job.ToApi();
return Ok();
}
@@ -341,7 +388,9 @@ namespace Tgstation.Server.Host.Controllers
{
Description = "Synchronize repository changes",
StartedBy = AuthenticationContext.User,
Instance = Instance
Instance = Instance,
CancelRightsType = RightsType.Repository,
CancelRight = (int)RepositoryRights.CancelSynchronize,
};
await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, ct) =>
{
@@ -349,6 +398,8 @@ namespace Tgstation.Server.Host.Controllers
if (repos != null)
await repos.Sychronize(accessString, ct).ConfigureAwait(false);
}, cancellationToken).ConfigureAwait(false);
api.ActiveJob = job.ToApi();
}
return Json(api);
@@ -104,6 +104,19 @@ namespace Tgstation.Server.Host.Core
{
var databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
job.StartedAt = DateTimeOffset.Now;
job.Instance = new Instance
{
Id = job.Instance.Id
};
databaseContext.Instances.Attach(job.Instance);
if (job.StartedBy != null)
{
job.StartedBy = new User
{
Id = job.StartedBy.Id
};
databaseContext.Users.Attach(job.StartedBy);
}
databaseContext.Jobs.Add(job);
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
var jobHandler = JobHandler.Create(x => RunJob(job, operation, x));