async Task <bool> LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action <Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken) { var repoSha = repository.Head; IQueryable <Models.RevisionInformation> ApplyQuery(IQueryable <Models.RevisionInformation> query) => query .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id) .Include(x => x.CompileJobs) .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy); var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (revisionInfo == default) { revisionInfo = databaseContext.RevisionInformations.Local.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id).FirstOrDefault(); } var needsDbUpdate = revisionInfo == default; if (needsDbUpdate) { // needs insertion revisionInfo = new Models.RevisionInformation { Instance = instance, CommitSha = repoSha, CompileJobs = new List <Models.CompileJob>(), ActiveTestMerges = new List <RevInfoTestMerge>() // non null vals for api returns }; lock (databaseContext) // cleaner this way databaseContext.RevisionInformations.Add(revisionInfo); } revisionInfo.OriginCommitSha = revisionInfo.OriginCommitSha ?? lastOriginCommitSha; if (revisionInfo.OriginCommitSha == null) { revisionInfo.OriginCommitSha = repoSha; Logger.LogWarning(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha); } revInfoSink?.Invoke(revisionInfo); return(needsDbUpdate); }
public override async Task <IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.Origin == null) { return(BadRequest(new ErrorMessage { Message = "Missing repo origin!" })); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" })); } var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); 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, StringComparison.Ordinal)) { model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal); } currentModel.AccessToken = model.AccessToken; currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm var cloneBranch = model.Reference; var origin = model.Origin; var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; if (repoManager.CloneInProgress) { return(Conflict(new ErrorMessage { Message = "A clone operation is in progress!" })); } if (repoManager.InUse) { return(Conflict(new ErrorMessage { Message = "The repo is busy!" })); } using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { // clone conflict if (repo != null) { return(Conflict(new ErrorMessage { Message = "The repository already exists!" })); } var job = new Models.Job { Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"), StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelClone, Instance = Instance }; var api = currentModel.ToApi(); await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false)) { if (repos == null) { throw new JobException("Filesystem conflict while cloning repository!"); } var instance = new Models.Instance { Id = Instance.Id }; databaseContext.Instances.Attach(instance); if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) { await databaseContext.Save(ct).ConfigureAwait(false); } } }, cancellationToken).ConfigureAwait(false); api.Origin = model.Origin; api.Reference = model.Reference; api.ActiveJob = job.ToApi(); return(StatusCode((int)HttpStatusCode.Created, api)); } }
#pragma warning disable CA1502 // TODO: Decomplexify #pragma warning disable CA1505 public override async Task <IActionResult> Update([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" })); } if (model.CheckoutSha != null && model.Reference != null) { return(BadRequest(new ErrorMessage { Message = "Only one of sha or reference may be specified!" })); } if (model.CheckoutSha != null && model.UpdateFromOrigin == true) { return(BadRequest(new ErrorMessage { Message = "Cannot update a reference when checking out a sha!" })); } if (model.Origin != null) { return(BadRequest(new ErrorMessage { Message = "origin cannot be modified without deleting the repository!" })); } if (model.NewTestMerges?.Any(x => !x.Number.HasValue) == true) { return(BadRequest(new ErrorMessage { Message = "All new test merges must provide a number!" })); } if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true) { return(BadRequest(new ErrorMessage { Message = "Cannot test merge the same PR twice in one job!" })); } if (model.CommitterName?.Length == 0) { return(BadRequest(new ErrorMessage { Message = "Cannot set empty committer name!" })); } if (model.CommitterEmail?.Length == 0) { return(BadRequest(new ErrorMessage { Message = "Cannot set empty committer e=mail!" })); } var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository); if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest)) { return(Forbid()); } var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (currentModel == default) { return(StatusCode((int)HttpStatusCode.Gone)); } bool CheckModified <T>(Expression <Func <Api.Models.Internal.RepositorySettings, T> > expression, RepositoryRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; var newVal = property.GetValue(model); if (newVal == null) { return(false); } if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal) { return(true); } property.SetValue(currentModel, newVal); return(false); } if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter) || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter) || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits) || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch))) { return(Forbid()); } if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0) { // setting an empty string clears everything currentModel.AccessUser = null; currentModel.AccessToken = null; } var canRead = userRights.HasFlag(RepositoryRights.Read); var api = canRead ? currentModel.ToApi() : new Repository(); var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; if (canRead) { if (repoManager.CloneInProgress) { return(Conflict(new ErrorMessage { Message = "A clone operation is in progress!" })); } if (repoManager.InUse) { return(Conflict(new ErrorMessage { Message = "The repo is busy!" })); } using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) { return(Conflict(new ErrorMessage { Message = "Repository could not be loaded!" })); } await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); } } // this is just db stuf so stow it away await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // format the job description string description = null; if (model.UpdateFromOrigin == true) { if (model.Reference != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); } else if (model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha); } else { description = "Pull current repository reference"; } } else if (model.Reference != null || model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha); } if (newTestMerges) { description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}", description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T", String.Join(", ", model.NewTestMerges.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number, x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))), description != null ? String.Empty : " in repository"); } if (description == null) { return(Json(api)); // no git changes } var job = new Models.Job { Description = description, StartedBy = AuthenticationContext.User, Instance = Instance, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false)) { if (repo == null) { throw new JobException("Repository could not be loaded!"); } var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null; var startReference = repo.Reference; var startSha = repo.Head; string postUpdateSha = null; if (newTestMerges && !repo.IsGitHubRepository) { throw new JobException("Cannot test merge on a non GitHub based repository!"); } var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName; var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var doneSteps = 0; Action <int> NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; return(progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps)); } progressReporter(0); // get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; var attachedInstance = new Models.Instance { Id = Instance.Id }; databaseContext.Instances.Attach(attachedInstance); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // apply new rev info, tracking applied test merges async Task UpdateRevInfo() { var last = lastRevisionInfo; await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, ct).ConfigureAwait(false); lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges); } try { // fetch/pull if (model.UpdateFromOrigin == true) { if (!repo.Tracking) { throw new JobException("Not on an updatable reference!"); } await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); doneSteps = 1; if (!modelHasShaOrReference) { var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false); if (!fastForward.HasValue) { throw new JobException("Merge conflict occurred during origin update!"); } await UpdateRevInfo().ConfigureAwait(false); if (fastForward.Value) { lastRevisionInfo.OriginCommitSha = repo.Head; await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); postUpdateSha = repo.Head; } else { NextProgressReporter()(100); } } } // checkout/hard reset if (modelHasShaOrReference) { if ((model.CheckoutSha != null && repo.Head.ToUpperInvariant().StartsWith(model.CheckoutSha.ToUpperInvariant(), StringComparison.Ordinal)) || (model.Reference != null && repo.Reference.ToUpperInvariant() != model.Reference.ToUpperInvariant())) { var committish = model.CheckoutSha ?? model.Reference; var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) { throw new JobException("Attempted to checkout a SHA or reference that was actually the opposite!"); } await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin } else { NextProgressReporter()(100); } if (hardResettingToOriginReference) { if (!repo.Tracking) { throw new JobException("Checked out reference does not track a remote object!"); } await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // repo head is on origin so force this // will update the db if necessary lastRevisionInfo.OriginCommitSha = repo.Head; } } // test merging Dictionary <int, Octokit.PullRequest> prMap = null; if (newTestMerges) { // bit of sanitization foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision))) { I.PullRequestRevision = null; } var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); var repoOwner = repo.GitHubOwner; var repoName = repo.GitHubRepoName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out Models.RevisionInformation revInfoWereLookingFor = null; bool needToApplyRemainingPrs = true; if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) { // In order for this to work though we need the shas of all the commits if (model.NewTestMerges.Any(x => x.PullRequestRevision == null)) { prMap = new Dictionary <int, Octokit.PullRequest>(); } bool cantSearch = false; foreach (var I in model.NewTestMerges) { if (I.PullRequestRevision != null) #pragma warning disable CA1308 // Normalize strings to uppercase { I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2 } #pragma warning restore CA1308 // Normalize strings to uppercase else { try { // retrieve the latest sha var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false); prMap.Add(I.Number.Value, pr); I.PullRequestRevision = pr.Head.Sha; } catch { cantSearch = true; break; } } } if (!cantSearch) { var dbPull = await databaseContext.RevisionInformations .Where(x => x.Instance.Id == Instance.Id && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges.Count <= model.NewTestMerges.Count && x.ActiveTestMerges.Count > 0) .Include(x => x.ActiveTestMerges) .ThenInclude(x => x.TestMerge) .ToListAsync(cancellationToken).ConfigureAwait(false); // split here cause this bit has to be done locally revInfoWereLookingFor = dbPull .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count && x.ActiveTestMerges.Select(y => y.TestMerge) .All(y => model.NewTestMerges.Any(z => y.Number == z.Number && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal) && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); if (revInfoWereLookingFor == null && model.NewTestMerges.Count > 1) { // okay try to add at least SOME prs we've seen before var search = model.NewTestMerges.ToList(); var appliedTestMergeIds = new List <long>(); Models.RevisionInformation lastGoodRevInfo = null; do { foreach (var I in search) { revInfoWereLookingFor = dbPull .Where(x => model.NewTestMerges.Any(z => x.PrimaryTestMerge.Number == z.Number && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal) && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)) && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id))) .FirstOrDefault(); if (revInfoWereLookingFor != null) { lastGoodRevInfo = revInfoWereLookingFor; appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id); search.Remove(I); break; } } }while (revInfoWereLookingFor != null && search.Count > 0); revInfoWereLookingFor = lastGoodRevInfo; needToApplyRemainingPrs = search.Count != 0; if (needToApplyRemainingPrs) { model.NewTestMerges = search; } } else if (revInfoWereLookingFor != null) { needToApplyRemainingPrs = false; } } } if (revInfoWereLookingFor != null) { // goteem await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false); lastRevisionInfo = revInfoWereLookingFor; } if (needToApplyRemainingPrs) { // an invocation of LoadRevisionInformation could have already loaded this user var contextUser = databaseContext.Users.Local.Where(x => x.Id == AuthenticationContext.User.Id).FirstOrDefault(); if (contextUser == default) { contextUser = new Models.User { Id = AuthenticationContext.User.Id }; databaseContext.Users.Attach(contextUser); } else { Logger.LogTrace("Skipping attaching the user to the database context as it is already loaded!"); } foreach (var I in model.NewTestMerges) { Octokit.PullRequest pr = null; string errorMessage = null; if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number.Value)) { throw new JobException("Cannot test merge the same PR twice in one HEAD!"); } try { // load from cache if possible if (prMap == null || !prMap.TryGetValue(I.Number.Value, out pr)) { pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false); } } catch (Octokit.RateLimitExceededException) { // you look at your anonymous access and sigh errorMessage = "P.R.E. RATE LIMITED"; } catch (Octokit.AuthorizationException) { errorMessage = "P.R.E. BAD CREDENTIALS"; } catch (Octokit.NotFoundException) { // you look at your shithub and sigh errorMessage = "P.R.E. NOT FOUND"; } // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it if (I.PullRequestRevision == null && pr != null) { I.PullRequestRevision = pr.Head.Sha; } var mergeResult = await repo.AddTestMerge(I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); if (!mergeResult.HasValue) { throw new JobException(String.Format(CultureInfo.InvariantCulture, "Merge of PR #{0} at {1} conflicted!", I.Number, I.PullRequestRevision.Substring(0, 7))); } ++doneSteps; var revInfoUpdateTask = UpdateRevInfo(); var tm = new Models.TestMerge { Author = pr?.User.Login ?? errorMessage, BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty, MergedAt = DateTimeOffset.Now, TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty, Comment = I.Comment, Number = I.Number, MergedBy = contextUser, PullRequestRevision = I.PullRequestRevision, Url = pr?.HtmlUrl ?? errorMessage }; await revInfoUpdateTask.ConfigureAwait(false); lastRevisionInfo.PrimaryTestMerge = tm; lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge { TestMerge = tm }); } } } var currentHead = repo.Head; if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } await databaseContext.Save(ct).ConfigureAwait(false); } catch { doneSteps = 0; numSteps = 2; // the stuff didn't make it into the db, forget what we've done and abort await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false); if (startReference != null && repo.Head != startSha) { await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); } else { progressReporter(100); } throw; } } }, cancellationToken).ConfigureAwait(false); api.ActiveJob = job.ToApi(); return(Accepted(api)); }
/// <summary> /// Construct a <see cref="CommandFactory"/> /// </summary> /// <param name="application">The value of <see cref="application"/></param> /// <param name="byondManager">The value of <see cref="byondManager"/></param> /// <param name="repositoryManager">The value of <see cref="repositoryManager"/></param> /// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param> /// <param name="instance">The value of <see cref="instance"/></param> public CommandFactory(IApplication application, IByondManager byondManager, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance) { this.application = application ?? throw new ArgumentNullException(nameof(application)); this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager)); this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); }
/// <inheritdoc /> #pragma warning disable CA1506 // TODO: Decomplexify public IInstance CreateInstance(Models.Instance metadata) { // Create the ioManager for the instance var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path); // various other ioManagers var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository"); var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond"); var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game"); var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration"); var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger <StaticFiles.Configuration>()); var eventConsumer = new EventConsumer(configuration); var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer, credentialsProvider, loggerFactory.CreateLogger <Repository.Repository>(), loggerFactory.CreateLogger <RepositoryManager>()); try { var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger <ByondManager>()); var commandFactory = new CommandFactory(application, byond, repoManager, databaseContextFactory, metadata); var chat = chatFactory.CreateChat(instanceIoManager, commandFactory, metadata.ChatSettings); try { var sessionControllerFactory = new SessionControllerFactory(processExecutor, byond, byondTopicSender, cryptographySuite, application, gameIoManager, chat, networkPromptReaper, platformIdentifier, loggerFactory, metadata.CloneMetadata()); var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger <DmbFactory>(), metadata.CloneMetadata()); try { var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, loggerFactory.CreateLogger <ReattachInfoHandler>(), metadata.CloneMetadata()); var watchdog = watchdogFactory.CreateWatchdog( chat, dmbFactory, reattachInfoHandler, configuration, sessionControllerFactory, gameIoManager, metadata.CloneMetadata(), metadata.DreamDaemonSettings); eventConsumer.SetWatchdog(watchdog); commandFactory.SetWatchdog(watchdog); try { var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, eventConsumer, chat, processExecutor, watchdog, loggerFactory.CreateLogger <DreamMaker>()); return(new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, jobManager, eventConsumer, gitHubClientFactory, loggerFactory.CreateLogger <Instance>())); } catch { watchdog.Dispose(); throw; } } catch { dmbFactory.Dispose(); throw; } } catch { chat.Dispose(); throw; } } catch { repoManager.Dispose(); throw; } }
#pragma warning disable CA1502, CA1505 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchUserAndAccessToken))); } if (model.CheckoutSha != null && model.Reference != null) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndReference))); } if (model.CheckoutSha != null && model.UpdateFromOrigin == true) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndUpdate))); } if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true) { return(BadRequest(new ErrorMessage(ErrorCode.RepoDuplicateTestMerge))); } if (model.CommitterName?.Length == 0) { return(BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterName))); } if (model.CommitterEmail?.Length == 0) { return(BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterEmail))); } var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository); if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest)) { return(Forbid()); } var currentModel = await DatabaseContext .RepositorySettings .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (currentModel == default) { return(Gone()); } bool CheckModified <T>(Expression <Func <Api.Models.Internal.RepositorySettings, T> > expression, RepositoryRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; var newVal = property.GetValue(model); if (newVal == null) { return(false); } if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal) { return(true); } property.SetValue(currentModel, newVal); return(false); } if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter) || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter) || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.CreateGitHubDeployments, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits) || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch))) { return(Forbid()); } if (model.AccessToken?.Length == 0 && model.AccessUser?.Length == 0) { // setting an empty string clears everything currentModel.AccessUser = null; currentModel.AccessToken = null; } var canRead = userRights.HasFlag(RepositoryRights.Read); var api = canRead ? currentModel.ToApi() : new Repository(); if (canRead) { var earlyOut = await WithComponentInstance( async instance => { var repoManager = instance.RepositoryManager; if (repoManager.CloneInProgress) { return(Conflict(new ErrorMessage(ErrorCode.RepoCloning))); } if (repoManager.InUse) { return(Conflict(new ErrorMessage(ErrorCode.RepoBusy))); } using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); if (repo == null) { return(Conflict(new ErrorMessage(ErrorCode.RepoMissing))); } await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); if (model.Origin != null && model.Origin != repo.Origin) { return(BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin))); } return(null); }) .ConfigureAwait(false); if (earlyOut != null) { return(earlyOut); } } // this is just db stuf so stow it away await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // format the job description string description = null; if (model.UpdateFromOrigin == true) { if (model.Reference != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); } else if (model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha); } else { description = "Pull current repository reference"; } } else if (model.Reference != null || model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha); } if (newTestMerges) { description = String.Format(CultureInfo.InvariantCulture, "{0}est merge(s) {1}{2}", description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T", String.Join(", ", model.NewTestMerges.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number, x.TargetCommitSha != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.TargetCommitSha.Substring(0, 7)) : String.Empty))), description != null ? String.Empty : " in repository"); } if (description == null) { return(Json(api)); // no git changes } async Task <IActionResult> UpdateCallbackThatDesperatelyNeedsRefactoring( IInstanceCore instance, IDatabaseContextFactory databaseContextFactory, Action <int> progressReporter, CancellationToken ct) { var repoManager = instance.RepositoryManager; using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false); if (repo == null) { throw new JobException(ErrorCode.RepoMissing); } var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null; var startReference = repo.Reference; var startSha = repo.Head; string postUpdateSha = null; if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown) { throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote); } var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName; var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var doneSteps = 0; Action <int> NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; return(progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps)); } progressReporter(0); // get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; var attachedInstance = new Models.Instance { Id = Instance.Id }; Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory .UseContext( async databaseContext => { databaseContext.Instances.Attach(attachedInstance); var previousRevInfo = lastRevisionInfo; var needsUpdate = await LoadRevisionInformation( repo, databaseContext, attachedInstance, lastOriginCommitSha, x => lastRevisionInfo = x, ct) .ConfigureAwait(false); if (testMergeToAdd != null) { // rev info may have already loaded the user var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id); if (mergedBy == default) { mergedBy = new Models.User { Id = AuthenticationContext.User.Id }; databaseContext.Users.Attach(mergedBy); } testMergeToAdd.MergedBy = mergedBy; foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges) { lastRevisionInfo.ActiveTestMerges.Add(activeTestMerge); } lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge { TestMerge = testMergeToAdd }); lastRevisionInfo.PrimaryTestMerge = testMergeToAdd; needsUpdate = true; } if (needsUpdate) { await databaseContext.Save(cancellationToken).ConfigureAwait(false); } }); await CallLoadRevInfo().ConfigureAwait(false); // apply new rev info, tracking applied test merges Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha); try { // fetch/pull if (model.UpdateFromOrigin == true) { if (!repo.Tracking) { throw new JobException(ErrorCode.RepoReferenceRequired); } await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); doneSteps = 1; if (!modelHasShaOrReference) { var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false); if (!fastForward.HasValue) { throw new JobException(ErrorCode.RepoMergeConflict); } lastRevisionInfo.OriginCommitSha = await repo.GetOriginSha(cancellationToken).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); if (fastForward.Value) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); postUpdateSha = repo.Head; } else { NextProgressReporter()(100); } } } // checkout/hard reset if (modelHasShaOrReference) { var validCheckoutSha = model.CheckoutSha != null && !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase); var validCheckoutReference = model.Reference != null && !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase); if (validCheckoutSha || validCheckoutReference) { var committish = model.CheckoutSha ?? model.Reference; var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) { throw new JobException(ErrorCode.RepoSwappedShaOrReference); } await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin } else { NextProgressReporter()(100); } if (hardResettingToOriginReference) { if (!repo.Tracking) { throw new JobException(ErrorCode.RepoReferenceNotTracking); } await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); await CallLoadRevInfo().ConfigureAwait(false); // repo head is on origin so force this // will update the db if necessary lastRevisionInfo.OriginCommitSha = repo.Head; } } // test merging if (newTestMerges) { if (repo.RemoteGitProvider == RemoteGitProvider.Unknown) { throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); } // bit of sanitization foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha))) { I.TargetCommitSha = null; } var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : gitHubClientFactory.CreateClient(); var repoOwner = repo.RemoteRepositoryOwner; var repoName = repo.RemoteRepositoryName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out Models.RevisionInformation revInfoWereLookingFor = null; bool needToApplyRemainingPrs = true; if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) { bool cantSearch = false; foreach (var I in model.NewTestMerges) { if (I.TargetCommitSha != null) #pragma warning disable CA1308 // Normalize strings to uppercase { I.TargetCommitSha = I.TargetCommitSha?.ToLowerInvariant(); // ala libgit2 } #pragma warning restore CA1308 // Normalize strings to uppercase else { try { // retrieve the latest sha var pr = await repo.GetTestMerge(I, currentModel, ct).ConfigureAwait(false); // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it I.TargetCommitSha = pr.TargetCommitSha; } catch { cantSearch = true; break; } } } if (!cantSearch) { List <Models.RevisionInformation> dbPull = null; await databaseContextFactory.UseContext( async databaseContext => dbPull = await databaseContext.RevisionInformations .AsQueryable() .Where(x => x.Instance.Id == Instance.Id && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges.Count <= model.NewTestMerges.Count && x.ActiveTestMerges.Count > 0) .Include(x => x.ActiveTestMerges) .ThenInclude(x => x.TestMerge) .ToListAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); // split here cause this bit has to be done locally revInfoWereLookingFor = dbPull .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count && x.ActiveTestMerges.Select(y => y.TestMerge) .All(y => model.NewTestMerges.Any(z => y.Number == z.Number && y.TargetCommitSha.StartsWith(z.TargetCommitSha, StringComparison.Ordinal) && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1) { // okay try to add at least SOME prs we've seen before var search = model.NewTestMerges.ToList(); var appliedTestMergeIds = new List <long>(); Models.RevisionInformation lastGoodRevInfo = null; do { foreach (var I in search) { revInfoWereLookingFor = dbPull .Where(testRevInfo => { if (testRevInfo.PrimaryTestMerge == null) { return(false); } var testMergeMatch = model.NewTestMerges.Any(testTestMerge => { var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number; if (!numberMatch) { return(false); } var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha.StartsWith( testTestMerge.TargetCommitSha, StringComparison.Ordinal); if (!shaMatch) { return(false); } var commentMatch = testRevInfo.PrimaryTestMerge.Comment == testTestMerge.Comment; return(commentMatch); }); if (!testMergeMatch) { return(false); } var previousTestMergesMatch = testRevInfo .ActiveTestMerges .Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge) .All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id)); return(previousTestMergesMatch); }) .FirstOrDefault(); if (revInfoWereLookingFor != null) { lastGoodRevInfo = revInfoWereLookingFor; appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id); search.Remove(I); break; } } }while (revInfoWereLookingFor != null && search.Count > 0); revInfoWereLookingFor = lastGoodRevInfo; needToApplyRemainingPrs = search.Count != 0; if (needToApplyRemainingPrs) { model.NewTestMerges = search; } } else if (revInfoWereLookingFor != null) { needToApplyRemainingPrs = false; } } } if (revInfoWereLookingFor != null) { // goteem Logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha); await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false); lastRevisionInfo = revInfoWereLookingFor; } if (needToApplyRemainingPrs) { foreach (var I in model.NewTestMerges) { if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number)) { throw new JobException(ErrorCode.RepoDuplicateTestMerge); } var fullTestMergeTask = repo.GetTestMerge(I, currentModel, ct); var mergeResult = await repo.AddTestMerge( I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); if (mergeResult == null) { throw new JobException( ErrorCode.RepoTestMergeConflict, new JobException( $"Test Merge #{I.Number} at {I.TargetCommitSha.Substring(0, 7)} conflicted!")); } Models.TestMerge fullTestMerge; try { fullTestMerge = await fullTestMergeTask.ConfigureAwait(false); } catch (Exception ex) { Logger.LogWarning("Error retrieving metadata for test merge #{0}!", I.Number); fullTestMerge = new Models.TestMerge { Author = ex.Message, BodyAtMerge = ex.Message, MergedAt = DateTimeOffset.UtcNow, TitleAtMerge = ex.Message, Comment = I.Comment, Number = I.Number, Url = ex.Message }; } // Ensure we're getting the full sha from git itself fullTestMerge.TargetCommitSha = I.TargetCommitSha; // MergedBy will be set later ++doneSteps; await UpdateRevInfo(fullTestMerge).ConfigureAwait(false); } } } var currentHead = repo.Head; if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } return(null); } catch { doneSteps = 0; numSteps = 2; // Forget what we've done and abort // DCTx2: Cancellation token is for job, operations should always run await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false); if (startReference != null && repo.Head != startSha) { await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); } else { progressReporter(100); } throw; } } var job = new Models.Job { Description = description, StartedBy = AuthenticationContext.User, Instance = Instance, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; // Time to access git, do it in a job await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressReporter, ct) => UpdateCallbackThatDesperatelyNeedsRefactoring( core, databaseContextFactory, progressReporter, ct), cancellationToken) .ConfigureAwait(false); api.ActiveJob = job.ToApi(); return(Accepted(api)); }
/// <summary> /// Corrects discrepencies between the <see cref="Api.Models.Instance.Online"/> status of <see cref="IInstance"/>s in the database vs the service. /// </summary> /// <param name="instanceManager">The <see cref="IInstanceManager"/> to use.</param> /// <param name="logger">The <see cref="ILogger"/> to use.</param> /// <param name="metadata">The <see cref="Models.Instance"/> to check.</param> /// <returns><see langword="true"/> if an unsaved DB update was made, <see langword="false"/> otherwise.</returns> public static bool ValidateInstanceOnlineStatus(IInstanceManager instanceManager, ILogger logger, Models.Instance metadata) { if (instanceManager == null) { throw new ArgumentNullException(nameof(instanceManager)); } if (metadata == null) { throw new ArgumentNullException(nameof(metadata)); } bool online; using (var instanceReferenceCheck = instanceManager.GetInstanceReference(metadata)) online = instanceReferenceCheck != null; if (metadata.Online.Value == online) { return(false); } const string OfflineWord = "offline"; const string OnlineWord = "online"; logger.LogWarning( "Instance {0} is says it's {1} in the database, but it is actually {2} in the service. Updating the database to reflect this...", metadata.Id, online ? OfflineWord : OnlineWord, online ? OnlineWord : OfflineWord); metadata.Online = online; return(true); }
static async Task <bool> PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action <Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken) { model.IsGitHub = repository.IsGitHubRepository; model.Origin = repository.Origin; model.Reference = repository.Reference; //rev info stuff Models.RevisionInformation revisionInfo = null; var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, lastOriginCommitSha, x => revisionInfo = x, cancellationToken).ConfigureAwait(false); model.RevisionInformation = revisionInfo.ToApi(); revInfoSink?.Invoke(revisionInfo); return(needsDbUpdate); }
public void TestConstruction() { Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(null, null, null, null, null, null, null, null, null, null, null, null, null, default)); var mockChat = new Mock <IChat>(); mockChat.Setup(x => x.RegisterCommandHandler(It.IsNotNull <ICustomCommandHandler>())).Verifiable(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, null, null, null, null, null, null, null, null, null, null, null, null, default)); var mockSessionControllerFactory = new Mock <ISessionControllerFactory>(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, null, null, null, null, null, null, null, null, null, null, null, default)); var mockDmbFactory = new Mock <IDmbFactory>(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, null, null, null, null, null, null, null, null, null, null, default)); var mockReattachInfoHandler = new Mock <IReattachInfoHandler>(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, null, null, null, null, null, null, null, null, null, default)); var mockDatabaseContextFactory = new Mock <IDatabaseContextFactory>(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, null, null, null, null, null, null, null, null, default)); var mockByondTopicSender = new Mock <IByondTopicSender>(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, null, null, null, null, null, null, null, default)); var mockEventConsumer = new Mock <IEventConsumer>(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, null, null, null, null, null, null, default)); var mockJobManager = new Mock <IJobManager>(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, mockJobManager.Object, null, null, null, null, null, default)); var mockRestartRegistration = new Mock <IRestartRegistration>(); mockRestartRegistration.Setup(x => x.Dispose()).Verifiable(); var mockServerControl = new Mock <IServerControl>(); mockServerControl.Setup(x => x.RegisterForRestart(It.IsNotNull <IRestartHandler>())).Returns(mockRestartRegistration.Object).Verifiable(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, mockJobManager.Object, mockServerControl.Object, null, null, null, null, default)); var mockAsyncDelayer = new Mock <IAsyncDelayer>(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, mockJobManager.Object, mockServerControl.Object, mockAsyncDelayer.Object, null, null, null, default)); var mockLogger = new Mock <ILogger <Watchdog> >(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, mockJobManager.Object, mockServerControl.Object, mockAsyncDelayer.Object, mockLogger.Object, null, null, default)); var mockLaunchParameters = new DreamDaemonLaunchParameters(); Assert.ThrowsException <ArgumentNullException>(() => new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, mockJobManager.Object, mockServerControl.Object, mockAsyncDelayer.Object, mockLogger.Object, mockLaunchParameters, null, default)); var mockInstance = new Models.Instance(); new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, mockJobManager.Object, mockServerControl.Object, mockAsyncDelayer.Object, mockLogger.Object, mockLaunchParameters, mockInstance, default).Dispose(); mockRestartRegistration.VerifyAll(); mockServerControl.VerifyAll(); mockChat.VerifyAll(); }
/// <summary> /// Pull the repository and compile for every set of given <paramref name="minutes"/> /// </summary> /// <param name="minutes">How many minutes the operation should repeat. Does not include running time</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> #pragma warning disable CA1502 // TODO: Decomplexify async Task TimerLoop(uint minutes, CancellationToken cancellationToken) { while (true) { try { await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false); logger.LogInformation("Beginning auto update..."); await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List <string>(), cancellationToken).ConfigureAwait(false); try { User user = null; await databaseContextFactory.UseContext( async (db) => user = await db .Users .AsQueryable() .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName)) .FirstAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); var repositoryUpdateJob = new Job { Instance = new Models.Instance { Id = metadata.Id }, Description = "Scheduled repository update", CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, StartedBy = user }; string deploySha = null; await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContextFactory, progressReporter, jobCancellationToken) => { // assume 5 steps with synchronize const int ProgressSections = 7; const int ProgressStep = 100 / ProgressSections; string repoHead = null; await databaseContextFactory.UseContext( async databaseContext => { var repositorySettingsTask = databaseContext .RepositorySettings .AsQueryable() .Where(x => x.InstanceId == metadata.Id) .FirstAsync(jobCancellationToken); const int NumSteps = 3; var doneSteps = 0; Action <int> NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; return(progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps)); } using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false); if (repo == null) { logger.LogTrace("Aborting repo update, no repository!"); return; } var startSha = repo.Head; if (!repo.Tracking) { logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!"); deploySha = startSha; return; } var repositorySettings = await repositorySettingsTask.ConfigureAwait(false); // the main point of auto update is to pull the remote await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); RevisionInformation currentRevInfo = null; bool hasDbChanges = false; Task <RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations .AsQueryable() .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) .FirstOrDefaultAsync(cancellationToken); async Task UpdateRevInfo(string currentHead, bool onOrigin) { if (currentRevInfo == null) { currentRevInfo = await LoadRevInfo().ConfigureAwait(false); } if (currentRevInfo == default) { logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead); onOrigin = true; } var attachedInstance = new Models.Instance { Id = metadata.Id }; var oldRevInfo = currentRevInfo; currentRevInfo = new RevisionInformation { CommitSha = currentHead, OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha, Instance = attachedInstance }; if (!onOrigin) { currentRevInfo.ActiveTestMerges = new List <RevInfoTestMerge>(oldRevInfo.ActiveTestMerges); } databaseContext.Instances.Attach(attachedInstance); databaseContext.RevisionInformations.Add(currentRevInfo); hasDbChanges = true; } // take appropriate auto update actions bool shouldSyncTracked; if (repositorySettings.AutoUpdatesKeepTestMerges.Value) { logger.LogTrace("Preserving test merges..."); var currentRevInfoTask = LoadRevInfo(); var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); if (!result.HasValue) { throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict); } currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha; var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit; var currentHead = repo.Head; if (currentHead != startSha) { await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false); shouldSyncTracked = stillOnOrigin; } else { shouldSyncTracked = false; } } else { logger.LogTrace("Not preserving test merges..."); await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); var currentHead = repo.Head; currentRevInfo = await databaseContext.RevisionInformations .AsQueryable() .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false); if (currentHead != startSha && currentRevInfo != default) { await UpdateRevInfo(currentHead, true).ConfigureAwait(false); } shouldSyncTracked = true; } // synch if necessary if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head) { var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false); var currentHead = repo.Head; if (currentHead != currentRevInfo.CommitSha) { await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false); } } repoHead = repo.Head; if (hasDbChanges) { try { await databaseContext.Save(cancellationToken).ConfigureAwait(false); } catch { await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false); throw; } } }) .ConfigureAwait(false); progressReporter(5 * ProgressStep); deploySha = repoHead; }, cancellationToken).ConfigureAwait(false); await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false); if (deploySha == null) { logger.LogTrace("Aborting auto update, repository error!"); continue; } if (deploySha == LatestCompileJob()?.RevisionInformation.CommitSha) { logger.LogTrace("Aborting auto update, same revision as latest CompileJob"); continue; } // finally set up the job var compileProcessJob = new Job { StartedBy = user, Instance = repositoryUpdateJob.Instance, Description = "Scheduled code deployment", CancelRightsType = RightsType.DreamMaker, CancelRight = (ulong)DreamMakerRights.CancelCompile }; await jobManager.RegisterOperation( compileProcessJob, DreamMaker.DeploymentProcess, cancellationToken).ConfigureAwait(false); await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false); } catch (OperationCanceledException) { logger.LogDebug("Cancelled auto update job!"); throw; } catch (Exception e) { logger.LogWarning("Error in auto update loop! Exception: {0}", e); continue; } } catch (OperationCanceledException) { break; } } logger.LogTrace("Leaving auto update loop..."); }
static async Task <bool> LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action <Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken) { var repoSha = repository.Head; IQueryable <Models.RevisionInformation> queryTarget = databaseContext.RevisionInformations; var revisionInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id) .Include(x => x.CompileJobs) .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) //minimal info, they can query the rest if they're allowed .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); //search every rev info because LOL SHA COLLISIONS if (revisionInfo == default) { revisionInfo = databaseContext.RevisionInformations.Local.Where(x => x.CommitSha == repoSha).FirstOrDefault(); } var needsDbUpdate = revisionInfo == default; if (needsDbUpdate) { //needs insertion revisionInfo = new Models.RevisionInformation { Instance = instance, CommitSha = repoSha, CompileJobs = new List <Models.CompileJob>(), ActiveTestMerges = new List <RevInfoTestMerge>() //non null vals for api returns }; lock (databaseContext) //cleaner this way databaseContext.RevisionInformations.Add(revisionInfo); } revisionInfo.OriginCommitSha = revisionInfo.OriginCommitSha ?? lastOriginCommitSha ?? repository.Head; revInfoSink?.Invoke(revisionInfo); return(needsDbUpdate); }
/// <inheritdoc /> #pragma warning disable CA1506 // TODO: Decomplexify public async Task <IInstance> CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata) { // Create the ioManager for the instance var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path); // various other ioManagers var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository"); var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond"); var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game"); var diagnosticsIOManager = new ResolvingIOManager(instanceIoManager, "Diagnostics"); var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration"); var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger <StaticFiles.Configuration>()); var eventConsumer = new EventConsumer(configuration); var repoManager = new RepositoryManager( repositoryFactory, repositoryCommands, repoIoManager, eventConsumer, loggerFactory.CreateLogger <Repository.Repository>(), loggerFactory.CreateLogger <RepositoryManager>()); try { var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger <ByondManager>()); var commandFactory = new CommandFactory(assemblyInformationProvider, byond, repoManager, databaseContextFactory, metadata); var chatManager = chatFactory.CreateChatManager(instanceIoManager, commandFactory, metadata.ChatSettings); try { var sessionControllerFactory = new SessionControllerFactory( processExecutor, byond, topicClientFactory, cryptographySuite, assemblyInformationProvider, gameIoManager, chatManager, networkPromptReaper, platformIdentifier, bridgeRegistrar, serverPortProvider, loggerFactory, loggerFactory.CreateLogger <SessionControllerFactory>(), metadata.CloneMetadata()); var gitHubDeploymentManager = new GitHubDeploymentManager( databaseContextFactory, gitHubClientFactory, loggerFactory.CreateLogger <GitHubDeploymentManager>(), metadata.CloneMetadata()); var dmbFactory = new DmbFactory( databaseContextFactory, gameIoManager, gitHubDeploymentManager, loggerFactory.CreateLogger <DmbFactory>(), metadata.CloneMetadata()); try { var reattachInfoHandler = new SessionPersistor( databaseContextFactory, dmbFactory, processExecutor, loggerFactory.CreateLogger <SessionPersistor>(), metadata.CloneMetadata()); var watchdog = watchdogFactory.CreateWatchdog( chatManager, dmbFactory, reattachInfoHandler, sessionControllerFactory, gameIoManager, diagnosticsIOManager, eventConsumer, gitHubDeploymentManager, metadata.CloneMetadata(), metadata.DreamDaemonSettings); eventConsumer.SetWatchdog(watchdog); commandFactory.SetWatchdog(watchdog); try { Instance instance = null; var dreamMaker = new DreamMaker( byond, gameIoManager, configuration, sessionControllerFactory, eventConsumer, chatManager, processExecutor, gitHubClientFactory, dmbFactory, repoManager, gitHubDeploymentManager, loggerFactory.CreateLogger <DreamMaker>(), metadata.CloneMetadata()); instance = new Instance( metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chatManager, configuration, dmbFactory, jobManager, eventConsumer, gitHubClientFactory, loggerFactory.CreateLogger <Instance>(), generalConfiguration); return(instance); } catch { await watchdog.DisposeAsync().ConfigureAwait(false); throw; } } catch { dmbFactory.Dispose(); throw; } } catch { await chatManager.DisposeAsync().ConfigureAwait(false); throw; } } catch { repoManager.Dispose(); throw; } }
public override async Task <IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (String.IsNullOrWhiteSpace(model.Name)) { return(BadRequest(new ErrorMessage { Message = "name must not be empty!" })); } if (model.Path == null) { return(BadRequest(new ErrorMessage { Message = "path must not be empty!" })); } NormalizeModelPath(model, out var rawPath); var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken); bool attached = false; if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false)) { if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false)) { return(Conflict(new ErrorMessage { Message = "Path not empty!" })); } else { attached = true; } } var newInstance = new Models.Instance { ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed, DreamDaemonSettings = new DreamDaemonSettings { AllowWebClient = false, AutoStart = false, PrimaryPort = 1337, SecondaryPort = 1338, SecurityLevel = DreamDaemonSecurity.Safe, SoftRestart = false, SoftShutdown = false, StartupTimeout = 20 }, DreamMakerSettings = new DreamMakerSettings { ApiValidationPort = 1339, ApiValidationSecurityLevel = DreamDaemonSecurity.Safe }, Name = model.Name, Online = false, Path = model.Path, AutoUpdateInterval = model.AutoUpdateInterval ?? 0, RepositorySettings = new RepositorySettings { CommitterEmail = "*****@*****.**", CommitterName = application.VersionPrefix, PushTestMergeCommits = false, ShowTestMergeCommitters = false, AutoUpdatesKeepTestMerges = false, AutoUpdatesSynchronize = false }, //give this user full privileges on the instance InstanceUsers = new List <Models.InstanceUser> { InstanceAdminUser() } }; DatabaseContext.Instances.Add(newInstance); try { await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); try { //actually reserve it now await ioManager.CreateDirectory(rawPath, cancellationToken).ConfigureAwait(false); await ioManager.DeleteFile(ioManager.ConcatPath(rawPath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false); } catch { //oh shit delete the model DatabaseContext.Instances.Remove(newInstance); await DatabaseContext.Save(default).ConfigureAwait(false);
public async Task <IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (String.IsNullOrWhiteSpace(model.Name)) { return(BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName))); } var targetInstancePath = NormalizePath(model.Path); model.Path = targetInstancePath; var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory); bool InstanceIsChildOf(string otherPath) { if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal)) { return(false); } bool sameLength = targetInstancePath.Length == otherPath.Length; char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)]; return(sameLength || dirSeparatorChar == Path.DirectorySeparatorChar || dirSeparatorChar == Path.AltDirectorySeparatorChar); } if (InstanceIsChildOf(installationDirectoryPath)) { return(Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath))); } // Validate it's not a child of any other instance IActionResult earlyOut = null; ulong countOfOtherInstances = 0; using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { var newCancellationToken = cts.Token; try { await DatabaseContext .Instances .AsQueryable() .Select(x => new Models.Instance { Path = x.Path }) .ForEachAsync( otherInstance => { if (++countOfOtherInstances >= generalConfiguration.InstanceLimit) { earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached)); } else if (InstanceIsChildOf(otherInstance.Path)) { earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath)); } if (earlyOut != null && !newCancellationToken.IsCancellationRequested) { cts.Cancel(); } }, newCancellationToken) .ConfigureAwait(false); } catch (OperationCanceledException) { cancellationToken.ThrowIfCancellationRequested(); } } if (earlyOut != null) { return(earlyOut); } // Last test, ensure it's in the list of valid paths if (!(generalConfiguration.ValidInstancePaths? .Select(path => NormalizePath(path)) .Any(path => InstanceIsChildOf(path)) ?? true)) { return(BadRequest(new ErrorMessage(ErrorCode.InstanceNotAtWhitelistedPath))); } async Task <bool> DirExistsAndIsNotEmpty() { if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(false)) { return(false); } var filesTask = ioManager.GetFiles(model.Path, cancellationToken); var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken); var files = await filesTask.ConfigureAwait(false); var dirs = await dirsTask.ConfigureAwait(false); return(files.Concat(dirs).Any()); } var dirExistsTask = DirExistsAndIsNotEmpty(); bool attached = false; if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false)) { if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false)) { return(Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath))); } else { attached = true; } } var newInstance = new Models.Instance { ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed, DreamDaemonSettings = new DreamDaemonSettings { AllowWebClient = false, AutoStart = false, PrimaryPort = 1337, SecondaryPort = 1338, SecurityLevel = DreamDaemonSecurity.Safe, StartupTimeout = 60, HeartbeatSeconds = 60 }, DreamMakerSettings = new DreamMakerSettings { ApiValidationPort = 1339, ApiValidationSecurityLevel = DreamDaemonSecurity.Safe }, Name = model.Name, Online = false, Path = model.Path, AutoUpdateInterval = model.AutoUpdateInterval ?? 0, ChatBotLimit = model.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit, RepositorySettings = new RepositorySettings { CommitterEmail = "*****@*****.**", CommitterName = assemblyInformationProvider.VersionPrefix, PushTestMergeCommits = false, ShowTestMergeCommitters = false, AutoUpdatesKeepTestMerges = false, AutoUpdatesSynchronize = false, PostTestMergeComment = false }, InstanceUsers = new List <Models.InstanceUser> // give this user full privileges on the instance { InstanceAdminUser() } }; DatabaseContext.Instances.Add(newInstance); try { await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); try { // actually reserve it now await ioManager.CreateDirectory(targetInstancePath, cancellationToken).ConfigureAwait(false); await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false); } catch { // oh shit delete the model DatabaseContext.Instances.Remove(newInstance); await DatabaseContext.Save(default).ConfigureAwait(false);
/// <summary> /// Construct a <see cref="PullRequestsCommand"/> /// </summary> /// <param name="watchdog">The value of <see cref="watchdog"/></param> /// <param name="repositoryManager">The value of <see cref="repositoryManager"/></param> /// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param> /// <param name="instance">The value of <see cref="instance"/></param> public PullRequestsCommand(IWatchdog watchdog, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance) { this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); }
public async Task TestSuccessfulLaunchAndShutdown() { var mockChat = new Mock <IChat>(); mockChat.Setup(x => x.RegisterCommandHandler(It.IsNotNull <ICustomCommandHandler>())).Verifiable(); var mockSessionControllerFactory = new Mock <ISessionControllerFactory>(); var mockDmbFactory = new Mock <IDmbFactory>(); var mockLogger = new Mock <ILogger <Watchdog> >(); var mockReattachInfoHandler = new Mock <IReattachInfoHandler>(); var mockDatabaseContextFactory = new Mock <IDatabaseContextFactory>(); var mockByondTopicSender = new Mock <IByondTopicSender>(); var mockEventConsumer = new Mock <IEventConsumer>(); var mockJobManager = new Mock <IJobManager>(); var mockRestartRegistration = new Mock <IRestartRegistration>(); mockRestartRegistration.Setup(x => x.Dispose()).Verifiable(); var mockServerControl = new Mock <IServerControl>(); mockServerControl.Setup(x => x.RegisterForRestart(It.IsNotNull <IRestartHandler>())).Returns(mockRestartRegistration.Object).Verifiable(); var mockLaunchParameters = new DreamDaemonLaunchParameters(); var mockInstance = new Models.Instance(); var mockAsyncDelayer = new Mock <IAsyncDelayer>(); using (var wd = new Watchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, mockJobManager.Object, mockServerControl.Object, mockAsyncDelayer.Object, mockLogger.Object, mockLaunchParameters, mockInstance, default)) using (var cts = new CancellationTokenSource()) { var mockCompileJob = new Models.CompileJob(); var mockDmbProvider = new Mock <IDmbProvider>(); mockDmbProvider.SetupGet(x => x.CompileJob).Returns(mockCompileJob).Verifiable(); var mDmbP = mockDmbProvider.Object; var infiniteTask = new TaskCompletionSource <int>().Task; mockDmbFactory.SetupGet(x => x.OnNewerDmb).Returns(infiniteTask); mockDmbFactory.Setup(x => x.LockNextDmb(2)).Returns(mDmbP).Verifiable(); var sessionsToVerify = new List <Mock <ISessionController> >(); var cancellationToken = cts.Token; mockSessionControllerFactory.Setup(x => x.LaunchNew(mockLaunchParameters, mDmbP, null, It.IsAny <bool>(), It.IsAny <bool>(), false, cancellationToken)).Returns(() => { var mockSession = new Mock <ISessionController>(); mockSession.SetupGet(x => x.Lifetime).Returns(infiniteTask).Verifiable(); mockSession.SetupGet(x => x.OnReboot).Returns(infiniteTask).Verifiable(); mockSession.SetupGet(x => x.Dmb).Returns(mDmbP).Verifiable(); mockSession.SetupGet(x => x.LaunchResult).Returns(Task.FromResult(new LaunchResult { StartupTime = TimeSpan.FromSeconds(1) })).Verifiable(); sessionsToVerify.Add(mockSession); return(Task.FromResult(mockSession.Object)); }).Verifiable(); mockAsyncDelayer.Setup(x => x.Delay(It.IsAny <TimeSpan>(), cancellationToken)).Returns(Task.CompletedTask).Verifiable(); cts.CancelAfter(TimeSpan.FromSeconds(15)); try { await wd.Launch(cancellationToken).ConfigureAwait(false); await wd.Terminate(false, cancellationToken).ConfigureAwait(false); } finally { cts.Cancel(); } Assert.AreEqual(2, sessionsToVerify.Count); foreach (var I in sessionsToVerify) { I.VerifyAll(); } mockDmbProvider.VerifyAll(); } mockSessionControllerFactory.VerifyAll(); mockDmbFactory.VerifyAll(); mockRestartRegistration.VerifyAll(); mockServerControl.VerifyAll(); mockChat.VerifyAll(); mockAsyncDelayer.VerifyAll(); }
async Task <bool> PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken) { if (repository.IsGitHubRepository) { model.GitHubOwner = repository.GitHubOwner; model.GitHubName = repository.GitHubRepoName; } model.Origin = repository.Origin; model.Reference = repository.Reference; //rev info stuff Models.RevisionInformation revisionInfo = null; var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken).ConfigureAwait(false); model.RevisionInformation = revisionInfo.ToApi(); return(needsDbUpdate); }
public async Task <IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.Origin == null) { return(BadRequest(ErrorCode.RepoMissingOrigin)); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(ErrorCode.RepoMismatchUserAndAccessToken)); } var currentModel = await DatabaseContext .RepositorySettings .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (currentModel == default) { return(Gone()); } currentModel.AccessToken = model.AccessToken; currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm var cloneBranch = model.Reference; var origin = model.Origin; return(await WithComponentInstance( async instance => { var repoManager = instance.RepositoryManager; if (repoManager.CloneInProgress) { return Conflict(new ErrorMessage(ErrorCode.RepoCloning)); } if (repoManager.InUse) { return Conflict(new ErrorMessage(ErrorCode.RepoBusy)); } using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); // clone conflict if (repo != null) { return Conflict(new ErrorMessage(ErrorCode.RepoExists)); } var job = new Models.Job { Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"), StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelClone, Instance = Instance }; var api = currentModel.ToApi(); await jobManager.RegisterOperation(job, async(core, databaseContextFactory, paramJob, progressReporter, ct) => { var repoManager = core.RepositoryManager; using var repos = await repoManager.CloneRepository( origin, cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, model.RecurseSubmodules ?? true, ct) .ConfigureAwait(false); if (repos == null) { throw new JobException(ErrorCode.RepoExists); } var instance = new Models.Instance { Id = Instance.Id }; await databaseContextFactory.UseContext( async databaseContext => { databaseContext.Instances.Attach(instance); if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) { await databaseContext.Save(ct).ConfigureAwait(false); } }) .ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false); api.Origin = model.Origin; api.Reference = model.Reference; api.ActiveJob = job.ToApi(); return Created(api); }) .ConfigureAwait(false)); }