示例#1
0
        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);
        }
示例#2
0
        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));
            }
        }
示例#3
0
                #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;
            }
        }
示例#6
0
#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));
        }
示例#7
0
        /// <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);
        }
示例#8
0
        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();
        }
示例#10
0
        /// <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...");
        }
示例#11
0
        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);
        }
示例#12
0
        /// <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;
            }
        }
示例#13
0
        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);
示例#14
0
        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);
示例#15
0
 /// <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);
        }
示例#18
0
        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));
        }