Refactor deployment to be completely within the DreamMaker component.

This commit is contained in:
Jordan Brown
2020-05-17 19:27:03 -04:00
parent fca44aef24
commit 422c48ee6c
11 changed files with 417 additions and 322 deletions
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Standard <see cref="IDmbFactory"/>
/// </summary>
sealed class DmbFactory : IDmbFactory, ICompileJobConsumer
sealed class DmbFactory : IDmbFactory, ICompileJobSink
{
/// <inheritdoc />
public Task OnNewerDmb
@@ -302,5 +302,13 @@ namespace Tgstation.Server.Host.Components.Deployment
await Task.WhenAll(tasks).ConfigureAwait(false);
}
#pragma warning restore CA1506
/// <inheritdoc />
public CompileJob LatestCompileJob()
{
if (!DmbAvailable)
return null;
return LockNextDmb(0)?.CompileJob;
}
}
}
@@ -1,4 +1,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Octokit;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -13,8 +15,11 @@ using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Components.Deployment
@@ -82,11 +87,31 @@ namespace Tgstation.Server.Host.Components.Deployment
/// </summary>
readonly IWatchdog watchdog;
/// <summary>
/// The <see cref="IRepositoryManager"/> for <see cref="DreamMaker"/>.
/// </summary>
readonly IRepositoryManager repositoryManager;
/// <summary>
/// The <see cref="IGitHubClientFactory"/> for <see cref="DreamMaker"/>.
/// </summary>
readonly IGitHubClientFactory gitHubClientFactory;
/// <summary>
/// The <see cref="ICompileJobSink"/> for <see cref="DreamMaker"/>.
/// </summary>
readonly ICompileJobSink compileJobConsumer;
/// <summary>
/// The <see cref="ILogger"/> for <see cref="DreamMaker"/>
/// </summary>
readonly ILogger<DreamMaker> logger;
/// <summary>
/// The <see cref="Instance"/> <see cref="DreamMaker"/> belongs to.
/// </summary>
readonly Api.Models.Instance metadata;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for <see cref="compiling"/>.
/// </summary>
@@ -108,7 +133,11 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="chatManager">The value of <see cref="chatManager"/></param>
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
/// <param name="watchdog">The value of <see cref="watchdog"/></param>
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/>.</param>
/// <param name="compileJobConsumer">The value of <see cref="compileJobConsumer"/>.</param>
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/>.</param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="metadata">The value of <see cref="metadata"/>.</param>
public DreamMaker(
IByondManager byond,
IIOManager ioManager,
@@ -118,7 +147,11 @@ namespace Tgstation.Server.Host.Components.Deployment
IChatManager chatManager,
IProcessExecutor processExecutor,
IWatchdog watchdog,
ILogger<DreamMaker> logger)
IGitHubClientFactory gitHubClientFactory,
ICompileJobSink compileJobConsumer,
IRepositoryManager repositoryManager,
ILogger<DreamMaker> logger,
Api.Models.Instance metadata)
{
this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
@@ -128,7 +161,11 @@ namespace Tgstation.Server.Host.Components.Deployment
this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager));
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
compilingLock = new object();
}
@@ -162,7 +199,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// </summary>
/// <param name="timeout">The timeout in seconds for validation</param>
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param>
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
/// <param name="job">The <see cref="CompileJob"/> for the operation</param>
/// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param>
/// <param name="portToUse">The port to use for API validation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
@@ -231,7 +268,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// Compiles a .dme with DreamMaker
/// </summary>
/// <param name="dreamMakerPath">The path to the DreamMaker executable</param>
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
/// <param name="job">The <see cref="CompileJob"/> for the operation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task<int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
@@ -260,7 +297,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Adds server side includes to the .dme being compiled
/// </summary>
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
/// <param name="job">The <see cref="CompileJob"/> for the operation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
@@ -314,7 +351,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Cleans up a failed compile <paramref name="job"/>
/// </summary>
/// <param name="job">The running <see cref="Models.CompileJob"/></param>
/// <param name="job">The running <see cref="CompileJob"/></param>
/// <param name="cancelled">If the <paramref name="job"/> was cancelled</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
@@ -338,7 +375,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Send a message to <see cref="chatManager"/> about a deployment
/// </summary>
/// <param name="revisionInformation">The <see cref="Models.RevisionInformation"/> for the deployment</param>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> for the deployment</param>
/// <param name="byondLock">The <see cref="IByondExecutableLock"/> for the deployment</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
@@ -396,7 +433,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Executes and populate a given <paramref name="job"/>
/// </summary>
/// <param name="job">The <see cref="Models.CompileJob"/> to run and populate</param>
/// <param name="job">The <see cref="CompileJob"/> to run and populate</param>
/// <param name="dreamMakerSettings">The <see cref="Api.Models.DreamMaker"/> settings to use</param>
/// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use</param>
/// <param name="repository">The <see cref="IRepository"/> to use</param>
@@ -484,14 +521,6 @@ namespace Tgstation.Server.Host.Components.Deployment
await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
await chatManager.SendUpdateMessage(
String.Format(
CultureInfo.InvariantCulture,
"Deployment complete! Changes will be applied when DreamDaemon {0}.",
watchdog.Running ? "reboots" : "is launched"),
cancellationToken)
.ConfigureAwait(false);
logger.LogDebug("Compile complete!");
}
catch (Exception e)
@@ -502,20 +531,180 @@ namespace Tgstation.Server.Host.Components.Deployment
}
/// <inheritdoc />
public async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
public async Task DeploymentProcess(
Models.Job job,
IDatabaseContext databaseContext,
Action<int> progressReporter,
CancellationToken cancellationToken)
{
if (revisionInformation == null)
throw new ArgumentNullException(nameof(revisionInformation));
if (dreamMakerSettings == null)
throw new ArgumentNullException(nameof(dreamMakerSettings));
if (repository == null)
throw new ArgumentNullException(nameof(repository));
#pragma warning disable IDE0016 // Use 'throw' expression
if (job == null)
throw new ArgumentNullException(nameof(job));
#pragma warning restore IDE0016 // Use 'throw' expression
if (databaseContext == null)
throw new ArgumentNullException(nameof(databaseContext));
if (progressReporter == null)
throw new ArgumentNullException(nameof(progressReporter));
var averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false);
var ddSettings = await databaseContext
.DreamDaemonSettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new Models.DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (ddSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
if (dreamMakerSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
Models.RepositorySettings repositorySettings = null;
string repoOwner = null;
string repoName = null;
Models.CompileJob compileJob;
Models.RevisionInformation revInfo;
using (var repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo == null)
throw new JobException(ErrorCode.RepoMissing);
if (repo.IsGitHubRepository)
{
repoOwner = repo.GitHubOwner;
repoName = repo.GitHubRepoName;
repositorySettings = await databaseContext
.RepositorySettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new Models.RepositorySettings
{
AccessToken = x.AccessToken,
ShowTestMergeCommitters = x.ShowTestMergeCommitters,
PushTestMergeCommits = x.PushTestMergeCommits,
PostTestMergeComment = x.PostTestMergeComment
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (repositorySettings == default)
throw new JobException(ErrorCode.InstanceMissingRepositorySettings);
}
var repoSha = repo.Head;
revInfo = await databaseContext
.RevisionInformations
.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id)
.Include(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ThenInclude(x => x.MergedBy)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (revInfo == default)
{
revInfo = new Models.RevisionInformation
{
CommitSha = repoSha,
OriginCommitSha = repoSha,
Instance = new Models.Instance
{
Id = metadata.Id
}
};
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
databaseContext.Instances.Attach(revInfo.Instance);
}
compileJob = await Compile(
revInfo,
dreamMakerSettings,
ddSettings.StartupTimeout.Value,
repo,
progressReporter,
averageSpan,
cancellationToken)
.ConfigureAwait(false);
try
{
compileJob.Job = job;
databaseContext.CompileJobs.Add(compileJob);
await PostDeploymentComments(compileJob, repositorySettings, repoOwner, repoName).ConfigureAwait(false);
// The difficulty with compile jobs is they have a two part commit
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
try
{
await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
}
catch
{
// So we need to un-commit the compile job if the above throws
databaseContext.CompileJobs.Remove(compileJob);
await databaseContext.Save(default).ConfigureAwait(false);
throw;
}
}
catch (Exception ex)
{
await CleanupFailedCompile(compileJob, ex is OperationCanceledException, default).ConfigureAwait(false);
throw;
}
}
await eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken).ConfigureAwait(false);
await chatManager.SendUpdateMessage(
String.Format(
CultureInfo.InvariantCulture,
"Deployment complete! Changes will be applied when DreamDaemon {0}.",
watchdog.Running ? "reboots" : "is launched"),
cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Calculate the average length of a deployment using a given <paramref name="databaseContext"/>.
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to retrieve previous deployment <see cref="Job"/>s from.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the average <see cref="TimeSpan"/> of the 10 previous deployments or <see langword="null"/> if there are none.</returns>
async Task<TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
{
var previousCompileJobs = await databaseContext.CompileJobs
.Where(x => x.Job.Instance.Id == metadata.Id)
.OrderByDescending(x => x.Job.StoppedAt)
.Take(10)
.Select(x => new Models.Job
{
StoppedAt = x.Job.StoppedAt,
StartedAt = x.Job.StartedAt
})
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
TimeSpan? averageSpan = null;
if (previousCompileJobs.Count != 0)
{
var totalSpan = TimeSpan.Zero;
foreach (var I in previousCompileJobs)
totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
averageSpan = totalSpan / previousCompileJobs.Count;
}
return averageSpan;
}
async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
{
logger.LogTrace("Begin Compile");
lock (compilingLock)
@@ -556,5 +745,99 @@ namespace Tgstation.Server.Host.Components.Deployment
await progressTask.ConfigureAwait(false);
}
}
/// <summary>
/// Post deployment GitHub comments.
/// </summary>
/// <param name="compileJob">The deployed <see cref="CompileJob"/>.</param>
/// <param name="repositorySettings">The <see cref="RepositorySettings"/>.</param>
/// <param name="repoOwner">The GitHub repostiory owner.</param>
/// <param name="repoName">The GitHub repostiory name.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task PostDeploymentComments(
Models.CompileJob compileJob,
Models.RepositorySettings repositorySettings,
string repoOwner,
string repoName)
{
if (repositorySettings?.AccessToken == null)
return;
// potential for commenting on a test merge change
var outgoingCompileJob = compileJobConsumer.LatestCompileJob();
if ((outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha == compileJob.RevisionInformation.CommitSha) || !repositorySettings.PostTestMergeComment.Value)
return;
outgoingCompileJob ??= new Models.CompileJob
{
RevisionInformation = new Models.RevisionInformation
{
ActiveTestMerges = new List<RevInfoTestMerge>()
}
};
var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
async Task CommentOnPR(int prNumber, string comment)
{
try
{
await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
}
catch (ApiException e)
{
logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
}
}
var tasks = new List<Task>();
string FormatTestMerge(Models.TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}",
Environment.NewLine,
repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
testMerge.PullRequestRevision,
testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
updated ? "Updated" : "Deployed",
metadata.Name,
compileJob.RevisionInformation.OriginCommitSha,
compileJob.RevisionInformation.CommitSha);
// added prs
foreach (var I in compileJob
.RevisionInformation
.ActiveTestMerges
.Select(x => x.TestMerge)
.Where(x => !outgoingCompileJob
.RevisionInformation
.ActiveTestMerges
.Any(y => y.TestMerge.Number == x.Number)))
tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false)));
// removed prs
foreach (var I in outgoingCompileJob
.RevisionInformation
.ActiveTestMerges
.Select(x => x.TestMerge)
.Where(x => !compileJob
.RevisionInformation
.ActiveTestMerges
.Any(y => y.TestMerge.Number == x.Number)))
tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed"));
// updated prs
foreach (var I in compileJob
.RevisionInformation
.ActiveTestMerges
.Select(x => x.TestMerge)
.Where(x => outgoingCompileJob
.RevisionInformation
.ActiveTestMerges
.Any(y => y.TestMerge.Number == x.Number)))
tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true)));
if (tasks.Any())
await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
}
@@ -1,6 +1,4 @@
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
@@ -9,10 +7,10 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Sink for <see cref="CompileJob"/>s
/// </summary>
public interface ICompileJobConsumer : IHostedService, IDisposable
public interface ICompileJobSink : ILatestCompileJobProvider
{
/// <summary>
/// Load a new <paramref name="job"/> into the <see cref="ICompileJobConsumer"/>
/// Load a new <paramref name="job"/> into the <see cref="ICompileJobSink"/>
/// </summary>
/// <param name="job">The <see cref="CompileJob"/> to load</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
@@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
@@ -8,7 +9,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Factory for <see cref="IDmbProvider"/>s
/// </summary>
public interface IDmbFactory
public interface IDmbFactory : ILatestCompileJobProvider, IHostedService, IDisposable
{
/// <summary>
/// Get a <see cref="Task"/> that completes when the result of a call to <see cref="LockNextDmb"/> will be different than the previous call if any
@@ -1,7 +1,8 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
@@ -11,16 +12,17 @@ namespace Tgstation.Server.Host.Components.Deployment
public interface IDreamMaker
{
/// <summary>
/// Starts a compile
/// Create and a compile job and insert it into the database. Meant to be called by a <see cref="Jobs.IJobManager"/>.
/// </summary>
/// <param name="revisionInformation">The <see cref="Models.RevisionInformation"/> being compiled from the <paramref name="repository"/></param>
/// <param name="dreamMakerSettings">The <see cref="Api.Models.DreamMaker"/> for the compile</param>
/// <param name="apiValidateTimeout">The time in seconds to wait while validating the API</param>
/// <param name="repository">The <see cref="IRepository"/> to copy from</param>
/// <param name="progressReporter">The <see cref="Action{T1}"/> to report compilation progress</param>
/// <param name="estimatedDuration">The estimated amount of time the compile will take</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the partially populated <see cref="Models.CompileJob"/> for the operation. In particular, note the <see cref="Models.CompileJob.RevisionInformation"/> field will only have it's <see cref="Api.Models.Internal.RevisionInformation.CommitSha"/> field populated</returns>
Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken);
/// <param name="job">The running <see cref="Job"/>.</param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the operation.</param>
/// <param name="progressReporter">The <see cref="Action{T1}"/> to report compilation progress.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task DeploymentProcess(
Job job,
IDatabaseContext databaseContext,
Action<int> progressReporter,
CancellationToken cancellationToken);
}
}
@@ -0,0 +1,16 @@
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
/// <summary>
/// Provides the most recently deployed <see cref="CompileJob"/>.
/// </summary>
public interface ILatestCompileJobProvider
{
/// <summary>
/// Gets the latest <see cref="CompileJob"/>.
/// </summary>
/// <returns>The latest <see cref="CompileJob"/>.</returns>
CompileJob LatestCompileJob();
}
}
@@ -1,21 +1,19 @@
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Components.Byond;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.StaticFiles;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// For interacting with the instance services
/// </summary>
public interface IInstance : IHostedService, IDisposable
public interface IInstance : ILatestCompileJobProvider, IHostedService, IDisposable
{
/// <summary>
/// The <see cref="IRepositoryManager"/> for the <see cref="IInstance"/>
@@ -27,6 +25,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
IByondManager ByondManager { get; }
/// <summary>
/// The <see cref="IDreamMaker"/> for the <see cref="IInstance"/>.
/// </summary>
IDreamMaker DreamMaker { get; }
/// <summary>
/// The <see cref="IWatchdog"/> for the <see cref="IInstance"/>
/// </summary>
@@ -42,12 +45,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
IConfiguration Configuration { get; }
/// <summary>
/// The latest staged <see cref="CompileJob"/>
/// </summary>
/// <returns>The latest <see cref="CompileJob"/> if it exists</returns>
CompileJob LatestCompileJob();
/// <summary>
/// Rename the <see cref="IInstance"/>
/// </summary>
@@ -60,15 +57,5 @@ namespace Tgstation.Server.Host.Components
/// <param name="newInterval">The new auto update inteval</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SetAutoUpdateInterval(uint newInterval);
/// <summary>
/// Run the compile job and insert it into the database. Meant to be called by a <see cref="Jobs.IJobManager"/>
/// </summary>
/// <param name="job">The running <see cref="Job"/></param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the operation</param>
/// <param name="progressReporter">The <see cref="Action{T1}"/> to report compilation progress</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken);
}
}
+20 -251
View File
@@ -1,9 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Octokit;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -13,7 +11,6 @@ using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
@@ -39,15 +36,8 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public StaticFiles.IConfiguration Configuration { get; }
/// <summary>
/// The <see cref="IDreamMaker"/> for the <see cref="Instance"/>
/// </summary>
readonly IDreamMaker dreamMaker;
/// <summary>
/// The <see cref="ICompileJobConsumer"/> for the <see cref="Instance"/>
/// </summary>
readonly ICompileJobConsumer compileJobConsumer;
/// <inheritdoc />
public IDreamMaker DreamMaker { get; }
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="Instance"/>
@@ -69,11 +59,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="IGitHubClientFactory"/> for the <see cref="Instance"/>
/// </summary>
readonly IGitHubClientFactory gitHubClientFactory;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Instance"/>
/// </summary>
@@ -105,16 +90,14 @@ namespace Tgstation.Server.Host.Components
/// <param name="metadata">The value of <see cref="metadata"/></param>
/// <param name="repositoryManager">The value of <see cref="RepositoryManager"/></param>
/// <param name="byondManager">The value of <see cref="ByondManager"/></param>
/// <param name="dreamMaker">The value of <see cref="dreamMaker"/></param>
/// <param name="dreamMaker">The value of <see cref="DreamMaker"/></param>
/// <param name="watchdog">The value of <see cref="Watchdog"/></param>
/// <param name="chat">The value of <see cref="Chat"/></param>
/// <param name="configuration">The value of <see cref="Configuration"/></param>
/// <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>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public Instance(
Api.Models.Instance metadata,
@@ -125,27 +108,23 @@ namespace Tgstation.Server.Host.Components
IChatManager chat,
StaticFiles.IConfiguration
configuration,
ICompileJobConsumer compileJobConsumer,
IDatabaseContextFactory databaseContextFactory,
IDmbFactory dmbFactory,
IJobManager jobManager,
IEventConsumer eventConsumer,
IGitHubClientFactory gitHubClientFactory,
ILogger<Instance> logger)
{
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
this.dreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
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.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
timerLock = new object();
@@ -155,227 +134,13 @@ namespace Tgstation.Server.Host.Components
public void Dispose()
{
timerCts?.Dispose();
compileJobConsumer.Dispose();
Configuration.Dispose();
Chat.Dispose();
Watchdog.Dispose();
dmbFactory.Dispose();
RepositoryManager.Dispose();
}
/// <inheritdoc />
public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken)
{
#pragma warning disable IDE0016 // Use 'throw' expression
if (job == null)
throw new ArgumentNullException(nameof(job));
#pragma warning restore IDE0016 // Use 'throw' expression
if (databaseContext == null)
throw new ArgumentNullException(nameof(databaseContext));
if (progressReporter == null)
throw new ArgumentNullException(nameof(progressReporter));
var ddSettings = await databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (ddSettings == default)
throw new JobException(Api.Models.ErrorCode.InstanceMissingDreamDaemonSettings);
var previousCompileJobs = await databaseContext.CompileJobs
.Where(x => x.Job.Instance.Id == metadata.Id)
.OrderByDescending(x => x.Job.StoppedAt)
.Select(x => new Job
{
StoppedAt = x.Job.StoppedAt,
StartedAt = x.Job.StartedAt
})
.Take(10)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
if (dreamMakerSettings == default)
throw new JobException(Api.Models.ErrorCode.InstanceMissingDreamMakerSettings);
RepositorySettings repositorySettings = null;
string repoOwner = null;
string repoName = null;
CompileJob compileJob;
RevisionInformation revInfo;
using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo == null)
throw new JobException(Api.Models.ErrorCode.RepoMissing);
if (repo.IsGitHubRepository)
{
repoOwner = repo.GitHubOwner;
repoName = repo.GitHubRepoName;
repositorySettings = await databaseContext
.RepositorySettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new RepositorySettings
{
AccessToken = x.AccessToken,
ShowTestMergeCommitters = x.ShowTestMergeCommitters,
PushTestMergeCommits = x.PushTestMergeCommits,
PostTestMergeComment = x.PostTestMergeComment
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (repositorySettings == default)
throw new JobException(Api.Models.ErrorCode.InstanceMissingRepositorySettings);
}
var repoSha = repo.Head;
revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy).FirstOrDefaultAsync().ConfigureAwait(false);
if (revInfo == default)
{
revInfo = new RevisionInformation
{
CommitSha = repoSha,
OriginCommitSha = repoSha,
Instance = new Models.Instance
{
Id = metadata.Id
}
};
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
databaseContext.Instances.Attach(revInfo.Instance);
}
TimeSpan? averageSpan = null;
if (previousCompileJobs.Count != 0)
{
var totalSpan = TimeSpan.Zero;
foreach (var I in previousCompileJobs)
totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
averageSpan = totalSpan / previousCompileJobs.Count;
}
compileJob = await dreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, progressReporter, averageSpan, cancellationToken).ConfigureAwait(false);
}
compileJob.Job = job;
databaseContext.CompileJobs.Add(compileJob);
await PostDeploymentComments(compileJob, repositorySettings, repoOwner, repoName).ConfigureAwait(false);
// The difficulty with compile jobs is they have a two part commit
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
try
{
await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
}
catch
{
// So we need to un-commit the compile job if the above throws
databaseContext.CompileJobs.Remove(compileJob);
await databaseContext.Save(default).ConfigureAwait(false);
throw;
}
await eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Post deployment GitHub comments.
/// </summary>
/// <param name="compileJob">The deployed <see cref="CompileJob"/>.</param>
/// <param name="repositorySettings">The <see cref="RepositorySettings"/>.</param>
/// <param name="repoOwner">The GitHub repostiory owner.</param>
/// <param name="repoName">The GitHub repostiory name.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task PostDeploymentComments(
CompileJob compileJob,
RepositorySettings repositorySettings,
string repoOwner,
string repoName)
{
if (repositorySettings?.AccessToken == null)
return;
// potential for commenting on a test merge change
var outgoingCompileJob = LatestCompileJob();
if ((outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha == compileJob.RevisionInformation.CommitSha) || !repositorySettings.PostTestMergeComment.Value)
return;
outgoingCompileJob ??= new CompileJob
{
RevisionInformation = new RevisionInformation
{
ActiveTestMerges = new List<RevInfoTestMerge>()
}
};
var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
async Task CommentOnPR(int prNumber, string comment)
{
try
{
await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
}
catch (ApiException e)
{
logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
}
}
var tasks = new List<Task>();
string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}",
Environment.NewLine,
repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
testMerge.PullRequestRevision,
testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
updated ? "Updated" : "Deployed",
metadata.Name,
compileJob.RevisionInformation.OriginCommitSha,
compileJob.RevisionInformation.CommitSha);
// added prs
foreach (var I in compileJob
.RevisionInformation
.ActiveTestMerges
.Select(x => x.TestMerge)
.Where(x => !outgoingCompileJob
.RevisionInformation
.ActiveTestMerges
.Any(y => y.TestMerge.Number == x.Number)))
tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false)));
// removed prs
foreach (var I in outgoingCompileJob
.RevisionInformation
.ActiveTestMerges
.Select(x => x.TestMerge)
.Where(x => !compileJob
.RevisionInformation
.ActiveTestMerges
.Any(y => y.TestMerge.Number == x.Number)))
tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed"));
// updated prs
foreach (var I in compileJob
.RevisionInformation
.ActiveTestMerges
.Select(x => x.TestMerge)
.Where(x => outgoingCompileJob
.RevisionInformation
.ActiveTestMerges
.Any(y => y.TestMerge.Number == x.Number)))
tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true)));
if (tasks.Any())
await Task.WhenAll(tasks).ConfigureAwait(false);
}
/// <summary>
/// Pull the repository and compile for every set of given <paramref name="minutes"/>
/// </summary>
@@ -393,11 +158,11 @@ namespace Tgstation.Server.Host.Components
await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
try
{
Models.User user = null;
User user = null;
await databaseContextFactory.UseContext(
async (db) => user = await db
.Users
.Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
.Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName))
.FirstAsync(cancellationToken)
.ConfigureAwait(false))
.ConfigureAwait(false);
@@ -582,7 +347,10 @@ namespace Tgstation.Server.Host.Components
CancelRight = (ulong)DreamMakerRights.CancelCompile
};
await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
await jobManager.RegisterOperation(
compileProcessJob,
DreamMaker.DeploymentProcess,
cancellationToken).ConfigureAwait(false);
await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
}
@@ -617,7 +385,13 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
await Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), compileJobConsumer.StartAsync(cancellationToken)).ConfigureAwait(false);
await Task.WhenAll(
SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value),
Configuration.StartAsync(cancellationToken),
ByondManager.StartAsync(cancellationToken),
Chat.StartAsync(cancellationToken),
dmbFactory.StartAsync(cancellationToken))
.ConfigureAwait(false);
// dependent on so many things, its just safer this way
await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
@@ -634,7 +408,7 @@ namespace Tgstation.Server.Host.Components
Configuration.StopAsync(cancellationToken),
ByondManager.StopAsync(cancellationToken),
Chat.StopAsync(cancellationToken),
compileJobConsumer.StopAsync(cancellationToken))
dmbFactory.StopAsync(cancellationToken))
.ConfigureAwait(false);
}
@@ -668,11 +442,6 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public CompileJob LatestCompileJob()
{
if (!dmbFactory.DmbAvailable)
return null;
return dmbFactory.LockNextDmb(0)?.CompileJob;
}
public CompileJob LatestCompileJob() => dmbFactory.LatestCompileJob();
}
}
@@ -254,9 +254,37 @@ namespace Tgstation.Server.Host.Components
commandFactory.SetWatchdog(watchdog);
try
{
var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, eventConsumer, chatManager, processExecutor, watchdog, loggerFactory.CreateLogger<DreamMaker>());
Instance instance = null;
var dreamMaker = new DreamMaker(
byond,
gameIoManager,
configuration,
sessionControllerFactory,
eventConsumer,
chatManager,
processExecutor,
watchdog,
gitHubClientFactory,
dmbFactory,
repoManager,
loggerFactory.CreateLogger<DreamMaker>(),
metadata.CloneMetadata());
return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chatManager, configuration, dmbFactory, databaseContextFactory, dmbFactory, jobManager, eventConsumer, gitHubClientFactory, loggerFactory.CreateLogger<Instance>());
instance = new Instance(
metadata.CloneMetadata(),
repoManager,
byond,
dreamMaker,
watchdog,
chatManager,
configuration,
databaseContextFactory,
dmbFactory,
jobManager,
eventConsumer,
loggerFactory.CreateLogger<Instance>());
return instance;
}
catch
{
@@ -127,7 +127,13 @@ namespace Tgstation.Server.Host.Controllers
CancelRight = (ulong)DreamMakerRights.CancelCompile,
Instance = Instance
};
await jobManager.RegisterOperation(job, instanceManager.GetInstance(Instance).CompileProcess, cancellationToken).ConfigureAwait(false);
IInstance instance = instanceManager.GetInstance(Instance);
await jobManager.RegisterOperation(
job,
instance.DreamMaker.DeploymentProcess,
cancellationToken)
.ConfigureAwait(false);
return Accepted(job.ToApi());
}
@@ -115,9 +115,6 @@
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>