app/controllers/ProjectsController.scala (172 lines of code) (raw):

package controllers import akka.actor.ActorSystem import akka.http.scaladsl.model.{ContentType, ContentTypes, HttpEntity, HttpResponse, MediaTypes, StatusCodes} import akka.stream.Materializer import auth.{BearerTokenAuth, Security} import play.api.Configuration import play.api.mvc.{AbstractController, ControllerComponents, ResponseHeader, Result} import services.{GithubAPI, GitlabAPI, HttpError, VCSAPI, ZipReader} import javax.inject.{Inject, Singleton} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import io.circe.syntax._ import io.circe.generic.auto._ import models.BuildInfo import models.gitlab.MergeRequestState import models.responses.GenericErrorResponse import org.slf4j.LoggerFactory import play.api.libs.circe.Circe import scalacache.memcached._ import scalacache.serialization.circe._ import scalacache.modes.scalaFuture._ import java.net.URLDecoder import java.nio.charset.StandardCharsets import java.time.ZonedDateTime import scala.concurrent.duration.Duration import scala.util.Try @Singleton class ProjectsController @Inject() (cc:ControllerComponents, gitLabAPI:GitlabAPI, gitHubAPI:GithubAPI, override val bearerTokenAuth:BearerTokenAuth, override val config:Configuration) (implicit actorSystem: ActorSystem, mat:Materializer) extends AbstractController(cc) with Circe with Security { private implicit val memcachedCache = MemcachedCache[BuildInfo](config.get[String]("memcached.location")) private val maybeCacheTTL = config.getOptional[Duration]("memcached.ttl") override protected val logger = LoggerFactory.getLogger(getClass) /** * takes a future from the gitlab API object and converts it into a Future[Result] * @param from the api response to convert * @tparam T data type of the domain object that the response returns * @return a play framework result */ def genericOutput[T:io.circe.Encoder](from:Future[Either[io.circe.Error, T]]) = from.map({ case Left(decodingError)=> logger.error(s"Could not decode response from server: $decodingError") InternalServerError(GenericErrorResponse("remote_error", decodingError.toString).asJson) case Right(apiContent)=> Ok(apiContent.asJson) }) .recover({ case err:Throwable=> logger.error(s"gitlab api operation failed: ${err.getMessage}", err) InternalServerError(GenericErrorResponse("error", err.getMessage).asJson) }) def knownProjects = IsAdminAsync { uid=> req=> genericOutput(gitLabAPI.listProjects) } def jobsForProject(projectId:String) = IsAdminAsync { uid=> req=> withVCSAPI(projectId) { vcs=> genericOutput(vcs.jobsForProject(projectId)) } } /** * Helper function that calls the given block with an appropriate Version Control System implementation * for the given ID. * If the ID is numeric, then it uses Gitlab, otherwise Github. * @param forId string ID to check * @param block Code to execute. This is passed a VCSAPI instance and is expected to return a Future of any kind * @tparam T kind of the data that is returned * @return whatever is returned from the block */ def withVCSAPI[T](forId:String)(block: (VCSAPI)=>Future[T]) = { ProjectIdHelper.numericId(forId) match { case Some(_)=>block(gitLabAPI) //numeric project ID=>gitlab case None=>block(gitHubAPI) //string project ID=>github } } def checkArtifacts(projectId:String, branchName:String, jobName:String) = IsAdminAsync { uid=> req=> withVCSAPI(projectId) { vcs => vcs.artifactsZipForBranch(projectId, branchName, jobName) .map({ case Some(bytes)=> val entity = play.api.http.HttpEntity.Strict(bytes, Some("application/zip")) Result( header = ResponseHeader(200, Map.empty), body = entity ) case None=> NotFound(GenericErrorResponse("not_found","not found").asJson) }) .recover({ case err: Throwable => logger.error(s"gitlab api operation failed: ${err.getMessage}", err) InternalServerError(GenericErrorResponse("error", err.getMessage).asJson) }) } } def getBuildInfo(projectId:String, branchName:String, jobName:String) = IsAdminAsync { uid=> req=> val cacheKey = s"$projectId-$branchName-$jobName" scalacache.get(cacheKey).flatMap({ case Some(buildInfo)=> logger.debug(s"Serving result for project id $projectId, branch $branchName, job $jobName from cache...") Future(Some(Right(buildInfo))) //we got a hit from the cache, don't bother going to the external service case None=> //nothing from the cache, look it up from the external service logger.debug(s"Serving result for project id $projectId, branch $branchName, job $jobName from origin...") findNewBuildInfo(projectId, branchName, jobName).flatMap({ case result @ Some(Right(buildInfo))=> scalacache.put(cacheKey)(buildInfo, maybeCacheTTL).map(_=>result) case result @ _=> Future(result) }) }).map({ case Some(Right(buildInfo))=> Ok(buildInfo.asJson) case Some(Left(parseErr))=> InternalServerError(GenericErrorResponse("invalid_data", s"extracted information failed to parse: $parseErr").asJson) case None=> NotFound(GenericErrorResponse("not_found", "no build info can be found for that job of that branch of that project. Consult the logs for more details").asJson) }).recover({ case httpErr:HttpError=> if(httpErr.getStatusCode==StatusCodes.NotFound) { NotFound(GenericErrorResponse("not_found", "build info is not available from this build").asJson) } else { logger.error(s"Could not extract build info: ${httpErr.getMessage}") InternalServerError(GenericErrorResponse("error", httpErr.getMessage).asJson) } case err:Throwable=> logger.error(s"Could not extract build info: ${err.getMessage}", err) InternalServerError(GenericErrorResponse("error", err.getMessage).asJson) }) } /** * extract the build-info.yaml information, if available, from gitlab * @param projectId project ID to query * @param branchName branch name to query * @param jobName pipeline job name that outputs build-info.yaml as an artefact * @return A Future, which contains None if there was no build-info available; Left if there was build-info which could * not be parsed; Right if there was parsable BuildInfo. */ def findNewBuildInfo(projectId:String, branchName:String, jobName:String) = { logger.debug(s"branchName is ${branchName}") logger.debug(s"jobName is $jobName") val maybeAwsAccount = config.getOptional[String]("aws.accountId") val maybeAwsRegion = config.getOptional[String]("aws.region") withVCSAPI(projectId) { vcs => val maybeZipContent = for { gitRef <- Future.fromTry(Try { URLDecoder.decode(branchName, StandardCharsets.UTF_8) }) zipContent <- vcs.artifactsZipForBranch(projectId, gitRef, jobName) } yield zipContent maybeZipContent.flatMap({ case Some(zipContent)=> for { zipReader <- Future(new ZipReader(zipContent.toArray)) maybeBuildInfo <- Future.fromTry(zipReader.locateBuildInfo(maybeAwsAccount, maybeAwsRegion)) } yield maybeBuildInfo case None=> Future(None) }) } } /** * proxy a request to the GitLab API for the recent branches of the project * @param projectId project ID to query */ def branchesForProject(projectId:String) = IsAdminAsync { uid=> req=> withVCSAPI(projectId) { vcs => vcs.branchesForProject(projectId.toString).map({ case Right(branches) => Ok(branches.sortBy(_.commit.committed_date).asJson) case Left(err) => logger.error(s"could not retrieve branches for project id $projectId: ${err.toString}") InternalServerError(GenericErrorResponse("error", s"gitlab api problem: ${err.toString}").asJson) }).recover({ case err: Throwable => logger.error(s"Get branches operation threw an exception: ${err.getMessage}", err) InternalServerError(GenericErrorResponse("internal_error", "An unexpected exception was thrown, see server logs for details").asJson) }) } } def mrForProject(projectId:String) = IsAdminAsync { uid=> req=> import models.gitlab.MergeRequestCodec._ withVCSAPI(projectId) { vcs => vcs.getOpenMergeRequests(projectId, Some(MergeRequestState.opened)).map({ case Right(mrs) => Ok(mrs.sortBy(_.created_at).asJson) case Left(err) => logger.error(s"Could not retrieve merge requests for project id $projectId: ${err.toString}") InternalServerError(GenericErrorResponse("error", s"gitlab api problem: ${err.toString}").asJson) }).recover({ case err: Throwable => logger.error(s"Get branches operation threw an exception: ${err.getMessage}", err) InternalServerError(GenericErrorResponse("internal_error", "An unexpected exception was thrown, see server logs for details").asJson) }) } } }