protected async Task OnJobEnded(BaseServer server, CiJob job)
        {
            ReleaseServerReservation(server);
            job.RunningOnServerId         = -1;
            job.RunningOnServerIsExternal = null;

            // After running the job, the changes saving should not be skipped
            await Database.SaveChangesAsync();

            // Send status to github
            var    status            = GithubAPI.CommitStatus.Success;
            string statusDescription = "Checks succeeded";

            if (!job.Succeeded)
            {
                status            = GithubAPI.CommitStatus.Failure;
                statusDescription = "Some checks failed";
            }

            if (job.Build?.CiProject == null)
            {
                throw new NotLoadedModelNavigationException();
            }

            if (!await StatusReporter.SetCommitStatus(job.Build.CiProject.RepositoryFullName, job.Build.CommitHash,
                                                      status, StatusReporter.CreateStatusUrlForJob(job), statusDescription,
                                                      job.JobName))
            {
                Logger.LogError("Failed to set commit status for build's job: {JobName}", job.JobName);
            }

            JobClient.Enqueue <CheckOverallBuildStatusJob>(x =>
                                                           x.Execute(job.CiProjectId, job.CiBuildId, CancellationToken.None));
        }
Пример #2
0
        public string CreateStatusUrlForJob(CiJob job)
        {
            if (baseUrl == null)
            {
                throw new InvalidOperationException("Base URL is not set");
            }

            return(new Uri(baseUrl, $"/ci/{job.CiProjectId}/build/{job.CiBuildId}/jobs/{job.CiJobId}").ToString());
        }
Пример #3
0
        private async Task CreateFailedJob(CiBuild build, string failure, CancellationToken cancellationToken)
        {
            var outputSection = new CiJobOutputSection()
            {
                CiProjectId          = build.CiProjectId,
                CiBuildId            = build.CiBuildId,
                CiJobId              = 1,
                CiJobOutputSectionId = 1,
                Name   = "Invalid configuration",
                Status = CIJobSectionStatus.Failed,
                Output = failure
            };

            outputSection.CalculateOutputLength();

            var job = new CiJob
            {
                CiProjectId = build.CiProjectId,
                CiBuildId   = build.CiBuildId,
                CiJobId     = 1,
                JobName     = "configuration_error",
                FinishedAt  = DateTime.UtcNow,
                Succeeded   = false,
                State       = CIJobState.Finished,

                CiJobOutputSections = new List <CiJobOutputSection>()
                {
                    outputSection
                }
            };

            if (build.CiProject == null)
            {
                throw new NotLoadedModelNavigationException();
            }

            await database.CiJobs.AddAsync(job, cancellationToken);

            await database.CiJobOutputSections.AddAsync(outputSection, cancellationToken);

            await database.SaveChangesAsync(cancellationToken);

            if (!await statusReporter.SetCommitStatus(build.CiProject.RepositoryFullName, build.CommitHash,
                                                      GithubAPI.CommitStatus.Failure, statusReporter.CreateStatusUrlForJob(job), failure,
                                                      job.JobName))
            {
                logger.LogError("Failed to report serious failed commit status with context {JobName}", job.JobName);
            }

            jobClient.Enqueue <CheckOverallBuildStatusJob>(x =>
                                                           x.Execute(build.CiProjectId, build.CiBuildId, CancellationToken.None));
        }
Пример #4
0
        private async Task Requeue(CiJob job, int retries, BaseServer server, bool serverIsExternal)
        {
            if (retries < 1)
            {
                Logger.LogError("CI build ran out of tries to try starting on the server");
                job.State      = CIJobState.Finished;
                job.FinishedAt = DateTime.UtcNow;
                job.Succeeded  = false;

                await job.CreateFailureSection(Database, "Connection to build server failed after multiple retries",
                                               "Server Failure");

                // Stop hogging the server and mark the build as failed
                await OnJobEnded(server, job);

                return;
            }

            JobClient.Schedule <RunJobOnServerJob>(x =>
                                                   x.Execute(job.CiProjectId, job.CiBuildId, job.CiJobId, server.Id, serverIsExternal, retries,
                                                             CancellationToken.None),
                                                   TimeSpan.FromSeconds(10));
        }
Пример #5
0
 private string GetConnectToUrl(CiJob job)
 {
     return(new Uri(configuration.GetBaseUrl(), $"/ciBuildConnection?key={job.BuildOutputConnectKey}")
            .ToString());
 }
Пример #6
0
        public async Task<IActionResult> ReRunBuild([Required] long projectId,
            [Required] long buildId, bool onlyFailed = true)
        {
            var build = await database.CiBuilds.Include(b => b.CiProject).Include(b => b.CiJobs)
                .FirstOrDefaultAsync(j => j.CiProjectId == projectId && j.CiBuildId == buildId);

            if (build == null)
                return NotFound();

            if (build.CiProject == null)
                throw new NotLoadedModelNavigationException();

            if (build.CiProject.Deleted)
                return NotFound();

            if (build.Status == BuildStatus.Running || build.Status == BuildStatus.GoingToFail)
                return BadRequest("Build can be rerun only after it is complete");

            var user = HttpContext.AuthenticatedUser()!;
            logger.LogInformation("CI build {ProjectId}-{BuildId} reran by {Email}", projectId, buildId, user.Email);

            // TODO: would be nice to have that non-admin action log type
            await database.LogEntries.AddAsync(new LogEntry()
            {
                Message = $"CI build reran {projectId}-{buildId} by a user",
                TargetUserId = user.Id,
            });

            build.Status = BuildStatus.Running;
            build.FinishedAt = null;

            // TODO: add a rerun counter to the build model

            // If there are no jobs, then the repo scan / result failed, so we might as well reset this and rerun
            // the repo scan
            if (build.CiJobs.Count < 1)
            {
                await database.SaveChangesAsync();

                jobClient.Enqueue<CheckAndStartCIBuild>(x => x.Execute(projectId, buildId, CancellationToken.None));
                return Ok("No jobs in this build, trying to re-run repo scan");
            }

            // Delete the jobs we are going to rerun
            // But first we need to grab the information from them needed to rerun the jobs (as we don't want to
            // re-check the repo here)
            long nextJobId = build.CiJobs.Max(j => j.CiJobId);
            var toRerun = build.CiJobs
                .Where(j => j.State == CIJobState.Finished && (onlyFailed && !j.Succeeded || !onlyFailed)).ToList();

            if (toRerun.Count < 1)
                return BadRequest("Nothing needs to rerun");

            foreach (var jobToRerun in toRerun)
            {
                var newJob = new CiJob()
                {
                    CiProjectId = jobToRerun.CiProjectId,
                    CiBuildId = jobToRerun.CiBuildId,
                    CiJobId = ++nextJobId,
                    JobName = jobToRerun.JobName,
                    Image = jobToRerun.Image,
                    CacheSettingsJson = jobToRerun.CacheSettingsJson,
                };

                await database.CiJobs.AddAsync(newJob);
            }

            // TODO: implement deleting job artifacts if those exist

            database.CiJobs.RemoveRange(toRerun);

            await database.SaveChangesAsync();

            jobClient.Enqueue<HandleControlledServerJobsJob>(x => x.Execute(CancellationToken.None));
            return Ok();
        }
Пример #7
0
        public async Task Execute(long ciProjectId, long ciBuildId, CancellationToken cancellationToken)
        {
            var build = await database.CiBuilds.Include(c => c.CiProject)
                        .FirstOrDefaultAsync(c => c.CiProjectId == ciProjectId && c.CiBuildId == ciBuildId, cancellationToken);

            if (build == null)
            {
                logger.LogError("Failed to find CIBuild to start");
                return;
            }

            // Update our local repo copy and see what the wanted build config is
            var semaphore = localTempFileLocks.GetTempFilePath($"ciRepos/{ciProjectId}", out string tempPath);

            await semaphore.WaitAsync(cancellationToken);

            var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
                               .Build();

            if (build.CiProject == null)
            {
                throw new NotLoadedModelNavigationException();
            }

            CiBuildConfiguration?configuration;

            try
            {
                await GitRunHelpers.EnsureRepoIsCloned(build.CiProject.RepositoryCloneUrl, tempPath, true,
                                                       cancellationToken);

                // Fetch the ref
                await GitRunHelpers.FetchRef(tempPath, build.RemoteRef, cancellationToken);

                // Then checkout the commit this build is actually for
                await GitRunHelpers.Checkout(tempPath, build.CommitHash, true, cancellationToken, true);

                // Clean out non-ignored files
                await GitRunHelpers.Clean(tempPath, cancellationToken);

                // Read the build configuration
                var text = await File.ReadAllTextAsync(Path.Join(tempPath, AppInfo.CIConfigurationFile), Encoding.UTF8,
                                                       cancellationToken);

                configuration = deserializer.Deserialize <CiBuildConfiguration>(text);
            }
            catch (Exception e)
            {
                configuration = null;
                logger.LogError("Error when trying to read repository for starting jobs: {@E}", e);
            }
            finally
            {
                semaphore.Release();
            }

            if (configuration == null)
            {
                await CreateFailedJob(build, "Failed to read repository or build configuration", cancellationToken);

                return;
            }

            // Check that configuration is valid
            var errors = new List <ValidationResult>();

            if (!Validator.TryValidateObject(configuration, new ValidationContext(configuration), errors))
            {
                logger.LogError("Build configuration object didn't pass validations, see following errors:");

                foreach (var error in errors)
                {
                    logger.LogError("Failure: {Error}", error);
                }

                // TODO: pass validation errors to the build output
                await CreateFailedJob(build, "Invalid configuration yaml", cancellationToken);

                return;
            }

            // TODO: refactor these checks to be cleaner
            if (configuration.Jobs.Select(j => j.Value.Cache).Any(c =>
                                                                  c.LoadFrom.Any(p => p.Contains("..") || p.StartsWith("/")) || c.WriteTo.Contains("..") ||
                                                                  c.WriteTo.Contains('/') ||
                                                                  (c.Shared != null && c.Shared.Any(s =>
                                                                                                    s.Key.Contains("..") || s.Key.StartsWith("/") || s.Value.Contains("..") ||
                                                                                                    s.Value.Contains('/'))) ||
                                                                  (c.System != null && c.System.Any(s =>
                                                                                                    s.Key.Contains("..") || s.Key.Contains('\'') || !s.Key.StartsWith("/") ||
                                                                                                    s.Value.Contains("..") || s.Value.Contains('/') || s.Value.Contains('\'')))))
            {
                logger.LogError("Build configuration cache paths have \"..\" in them or starts with a slash");

                await CreateFailedJob(build, "Invalid configuration yaml, forbidden cache path", cancellationToken);

                return;
            }

            if (configuration.Jobs.Select(j => j.Value.Image).Any(i => i.Contains("..") || i.StartsWith("/")))
            {
                logger.LogError("Build configuration image names have \"..\" in them or starts with a slash");

                await CreateFailedJob(build, "Invalid configuration yaml, forbidden image name", cancellationToken);

                return;
            }

            if (configuration.Jobs.SelectMany(j => j.Value.Artifacts.Paths).Any(p =>
                                                                                p.Length < 3 || p.Length > 250 || p.StartsWith("/") || p.Contains("..")))
            {
                logger.LogError("Build has a too long, short, or non-relative artifact path");

                await CreateFailedJob(build, "Invalid configuration yaml, invalid artifact path(s)", cancellationToken);

                return;
            }

            if (configuration.Jobs.Any(j => j.Key == "CLA"))
            {
                logger.LogError("Build configuration job contains 'CLA' in it");

                await CreateFailedJob(build, "Invalid configuration yaml, forbidden job name", cancellationToken);

                return;
            }

            // TODO: do something with the version number here...

            // Then queue the jobs we found in the configuration
            var  jobs  = new List <CiJob>();
            long jobId = 0;

            foreach (var jobEntry in configuration.Jobs)
            {
                if (string.IsNullOrWhiteSpace(jobEntry.Key) || jobEntry.Key.Length > 80)
                {
                    await CreateFailedJob(build, "Invalid job name in configuration", cancellationToken);

                    return;
                }

                var job = new CiJob()
                {
                    CiProjectId       = ciProjectId,
                    CiBuildId         = ciBuildId,
                    CiJobId           = ++jobId,
                    JobName           = jobEntry.Key,
                    Image             = jobEntry.Value.Image,
                    CacheSettingsJson = JsonSerializer.Serialize(jobEntry.Value.Cache),
                };

                await database.CiJobs.AddAsync(job, cancellationToken);

                jobs.Add(job);
            }

            await database.SaveChangesAsync(cancellationToken);

            // Send statuses to github
            foreach (var job in jobs)
            {
                if (!await statusReporter.SetCommitStatus(build.CiProject.RepositoryFullName, build.CommitHash,
                                                          GithubAPI.CommitStatus.Pending, statusReporter.CreateStatusUrlForJob(job), "CI checks starting",
                                                          job.JobName))
                {
                    logger.LogError("Failed to set commit status for a build's job: {JobName}", job.JobName);
                }
            }

            // Queue remote executor check task which will allocate a server to run the job(s) on
            jobClient.Enqueue <HandleControlledServerJobsJob>(x => x.Execute(CancellationToken.None));
        }