app/controllers/Api.scala (62 lines of code) (raw):

/* * Copyright 2014 The Guardian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package controllers import com.github.blemale.scaffeine.{LoadingCache, Scaffeine} import com.madgag.scalagithub.model.RepoId import lib._ import lib.actions.Parsers.parseGitHubHookJson import play.api.libs.json.{JsArray, JsNumber} import play.api.mvc._ import scala.concurrent.Future class Api( scanSchedulerFactory: ScanScheduler.Factory, repoAcceptListService: RepoAcceptListService, delayer: Delayer, cc: ControllerAppComponents ) extends AbstractAppController(cc) { def githubHook() = Action.async(parse.json) { implicit request => val githubDeliveryGuid = request.headers.get("X-GitHub-Delivery") val eventOpt: Option[String] = request.headers.get("X-Github-Event") val repoIdOpt = Option.unless(eventOpt.contains("ping"))(parseGitHubHookJson(request.body)) logger.info(s"githubHook event=${eventOpt.getOrElse("unknown")} repo=${repoIdOpt.map(_.fullName)} githubDeliveryGuid=$githubDeliveryGuid xRequestId=$xRequestId") repoIdOpt.fold(Future.successful(Ok("pong")))(updateForRepo) } def updateRepo(repoId: RepoId) = Action.async { implicit request => logger.info(s"updateRepo repo=${repoId.fullName} xRequestId=$xRequestId") updateForRepo(repoId) } def xRequestId(implicit request: RequestHeader): Option[String] = request.headers.get("X-Request-ID") def updateForRepo(repoId: RepoId): Future[Result] = { logger.debug(s"update requested for $repoId") for { acceptList <- repoAcceptListService.acceptList() update <- updateFor(repoId, acceptList) } yield update } val repoScanSchedulerCache: LoadingCache[RepoId, ScanScheduler] = Scaffeine() .recordStats() .maximumSize(500) .build(scanSchedulerFactory.createFor) def updateFor(repoId: RepoId, acceptList: RepoAcceptList): Future[Result] = { val scanGuardF = Future { // wrapped in a future to avoid timing attacks val knownRepo = acceptList.allKnownRepos(repoId) logger.info(s"$repoId known=$knownRepo") require(knownRepo, s"${repoId.fullName} not on known-repo whitelist") val scanScheduler = repoScanSchedulerCache.get(repoId) logger.debug(s"$repoId scanScheduler=$scanScheduler") val firstScanF = scanScheduler.scan() firstScanF.onComplete { _ => delayer.delayTheFuture { /* Do a *second* scan shortly after the first one ends, to cope with: * 1. Latency in GH API * 2. Checkpoint site stabilising on the new version after deploy */ scanScheduler.scan() } } firstScanF } val mightBePrivate = !acceptList.publicRepos(repoId) if (mightBePrivate) { // Response must be immediate, with no private information (e.g. even acknowledging that repo exists) Future.successful(NoContent) } else { // we can delay the response to return information about the repo config, and the updates generated for { scanGuard <- scanGuardF scan <- scanGuard } yield Ok(JsArray(scan.map(summary => JsNumber(summary.prCheckpointDetails.pr.number)))) } } }