blob: cbb72ac9a58ae1cb18d9dfe626b27931acb13df6 [file] [log] [blame]
// Copyright (C) 2019 GerritForge Ltd
//
// 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 com.gerritforge.analytics.auditlog.broadcast
import java.net.URLDecoder
import com.gerritforge.analytics.auditlog.util.RegexUtil
import com.gerritforge.analytics.common.api.GerritConnectivity
import com.gerritforge.analytics.support.ops.GerritSourceOps._
import com.typesafe.scalalogging.LazyLogging
import org.json4s.native.JsonMethods._
import org.json4s.{DefaultFormats, _}
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
case class GerritProject(name: GerritProjectName)
case class GerritProjects(private val projects: Map[GerritProjectName, GerritProject]) extends LazyLogging with RegexUtil {
private val PROJECT_SSH_WITH_SPACES = capture(r = """project:(.+?)\)?\s""")
private val PROJECT_SSH_WITH_BRACKETS = capture(r = """project:\{\^?(.*)\}""")
private val PROJECT_SSH_NO_SPACES = capture(r = """project(?::|.)(.*)""")
private val PROJECT_SSH_PACK = capture(r = """(?:git-receive-pack|git-upload-pack)\.\/(.+)""")
private val PROJECT_SSH_REPLICATION_START = capture(r = """replication\.start\.([\.\/a-zA-Z0-9]+(?:-[a-zA-Z0-9\.]+)*)(?:\.|\s|$)""")
private val PROJECT_REST_API_CHANGES_SEGMENT = capture(r = """changes\/([^\/]+)~""")
private val PROJECT_REST_API_PROJECTS_SEGMENT = capture(r = """projects\/([^\/]+)""")
private val PROJECT_HTTP_PACK_INFO_REF = capture(r = """https?:\/\/(.+)\/info\/refs\?service=(?:git-upload-pack|git-receive-pack)""")
private val PROJECT_HTTP_PACK = capture(r = """https?:\/\/(.+)\/(?:git-upload-pack|git-receive-pack)""")
private val NO_PROJECT_RELATED_COMMANDS = capture(r = """(LOGIN|LOGOUT|AUTH)""")
private def existProject(id: GerritProjectName): Boolean = projects.get(id).isDefined
// Helper method to find a project at the start of a string.
// For example, the string `redhat-performance/quads.github.com.status:open.foo:bar` will match the project
// redhat-performance/quads.github.com, if that project exists in `gerritProject`
private def findProjectStringAtStart(rawProject: String, sep: Char = '.'): Option[String] =
rawProject.split(sep).foldLeft(List.empty[String]) { case (acc, token) =>
acc.headOption.map { t => (t + sep + token) +: acc }.getOrElse(List(token))
}.find(existProject)
// Helper method to find a project at the end of a string.
// For example, the string `gerrit.foo.bar.baz.redhat-performance/quads.github.com` will match the project
// redhat-performance/quads.github.com, if that project exists in `gerritProject`
private def findProjectStringAtEnd(rawProject: String, sep: Char = '.'): Option[String] =
rawProject.split(sep).foldRight(List.empty[String]) { case (token, acc) =>
acc.headOption.map { t => (token + sep + t) +: acc }.getOrElse(List(token))
}.find(existProject)
def extractProject(what: String, accessPath: String): Option[String] = accessPath match {
case _ if matches(NO_PROJECT_RELATED_COMMANDS, what) =>
None
case "SSH_COMMAND" =>
extractGroup(PROJECT_SSH_WITH_SPACES, what)
.orElse(extractGroup(PROJECT_SSH_WITH_BRACKETS, what))
.orElse(extractGroup(PROJECT_SSH_PACK, what))
.orElse(extractGroup(PROJECT_SSH_REPLICATION_START, what))
.orElse(extractGroup(PROJECT_SSH_NO_SPACES, what).flatMap(findProjectStringAtStart(_)))
.orElse(findProjectStringAtStart(what))
.orElse(findProjectStringAtEnd(what))
case "REST_API" | "UNKNOWN" =>
extractGroup(PROJECT_REST_API_CHANGES_SEGMENT, what)
.orElse(extractGroup(PROJECT_REST_API_PROJECTS_SEGMENT, what))
.map(URLDecoder.decode(_, "UTF-8"))
case "GIT" =>
extractGroup(PROJECT_HTTP_PACK_INFO_REF, what)
.orElse(extractGroup(PROJECT_HTTP_PACK, what))
.flatMap(findProjectStringAtEnd(_, '/'))
case unexpected =>
logger.warn(s"Unexpected access path '$unexpected' encountered when extracting project from '$what'")
None
}
}
object GerritProjects extends LazyLogging {
val empty = GerritProjects(Map.empty[GerritProjectName, GerritProject])
implicit private val formats = DefaultFormats
def loadProjects(gerritConnectivity: GerritConnectivity, gerritUrl: String): Try[GerritProjects] = {
val PAGE_SIZE = 500
logger.debug(s"Loading gerrit projects...")
val baseUrl = s"""$gerritUrl/projects/?n=$PAGE_SIZE&query=state%3Aactive%20OR%20state%3Aread-only"""
@tailrec
def loopThroughPages(more: Boolean, triedAcc: Try[GerritProjects] = Success(empty)): Try[GerritProjects] = {
if (!more)
triedAcc
else {
val acc = triedAcc.get
val url = baseUrl + s"&S=${acc.projects.size}"
val accountsJsonPage = gerritConnectivity.getContentFromApi(url).dropGerritPrefix.mkString
logger.debug(s"Getting gerrit projects - start: ${acc.projects.size}")
val pageInfo = Try(parse(accountsJsonPage)).map { jsMapProjects =>
val thisPageProjects = jsMapProjects.extract[List[GerritProject]].map(prj => prj.name -> prj)
(thisPageProjects.size == PAGE_SIZE, acc.copy(projects = acc.projects ++ thisPageProjects))
}
pageInfo match {
case Success((newMore, newGerritProjects)) => loopThroughPages(newMore, Success(newGerritProjects))
case Failure(exception) => loopThroughPages(more=false, Failure(exception))
}
}
}
loopThroughPages(more=true)
}
}