blob: e545925c7d848f0c9eee867b00f74f8e6d27f9d2 [file] [log] [blame]
// Copyright (C) 2017 The Android Open Source Project
//
// 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.googlesource.gerrit.plugins.analytics.wizard
import java.net.URLEncoder
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.Path
import com.google.common.io.ByteStreams
import com.google.gerrit.extensions.annotations.PluginData
import com.google.gerrit.extensions.restapi.{
Response,
RestApiException,
RestModifyView,
RestReadView
}
import com.google.gerrit.server.config.GerritServerConfig
import com.google.gerrit.server.project.ProjectResource
import com.google.inject.{ImplementedBy, Inject}
import com.googlesource.gerrit.plugins.analytics.wizard.AnalyticDashboardSetup.writer
import com.googlesource.gerrit.plugins.analytics.wizard.model.{
ETLConfig,
ETLConfigRaw,
ETLConfigValidationError
}
import com.googlesource.gerrit.plugins.analytics.wizard.utils._
import com.spotify.docker.client.messages.ContainerInfo
import com.spotify.docker.client.{DefaultDockerClient, DockerClient}
import org.eclipse.jgit.lib.Config
import scala.util.{Failure, Success}
case class Input(dashboardName: String, etlConfig: ETLConfigRaw)
class PutAnalyticsStack @Inject()(@PluginData val dataPath: Path,
@GerritServerConfig gerritConfig: Config)
extends RestModifyView[ProjectResource, Input] {
override def apply(resource: ProjectResource, input: Input): Response[String] = {
val projectName = resource.getName
val encodedName = AnalyticsWizardActions.encodedName(projectName)
val etlConfigE: Either[ETLConfigValidationError, ETLConfig] = ETLConfig.fromRaw(input.etlConfig)
etlConfigE.fold(
configError =>
Response.withStatusCode(400,
s"Cannot create dashboard configuration: ${configError.message}"),
etlConfig => {
val configHelper = new GerritConfigHelper(gerritConfig) with LocalAddressGetter
configHelper.getGerritLocalAddress match {
case Success(gerritLocalUrl) =>
AnalyticDashboardSetup(
input.dashboardName,
dataPath.resolve(s"docker-compose.${input.dashboardName}.yaml"),
gerritLocalUrl,
etlConfig
).createDashboardSetupFile()
Response.created(s"Dashboard configuration created for $encodedName!")
case Failure(exception) =>
Response.withStatusCode(
500,
s"Cannot create dashboard configuration - '${exception.getMessage}'")
}
}
)
}
}
class DockerComposeCommand(var action: String, var dashboardName: String)
class PostAnalyticsStack @Inject()(@PluginData val dataPath: Path)
extends RestModifyView[ProjectResource, DockerComposeCommand] {
override def apply(resource: ProjectResource, input: DockerComposeCommand): Response[String] = {
val encodedName = AnalyticsWizardActions
.encodedName(input.dashboardName)
val pb = new ProcessBuilder(
"docker-compose",
"-f",
s"${dataPath.toFile.getAbsolutePath}/docker-compose.${encodedName}.yaml",
input.action.toLowerCase,
"--detach"
)
pb.redirectErrorStream(true)
val ps: Process = pb.start
ps.getOutputStream.close
val output =
new String(ByteStreams.toByteArray(ps.getInputStream), UTF_8)
ps.waitFor
ps.exitValue match {
case 0 => Response.created(output)
case _ =>
throw new RestApiException(s"Failed with exit code: ${ps.exitValue} - $output")
}
}
}
class GetAnalyticsStackStatus @Inject()(@PluginData val dataPath: Path,
val dockerClientProvider: DockerClientProvider)
extends RestReadView[ProjectResource] {
override def apply(resource: ProjectResource): Response[String] = {
val containerName = "gerrit-analytics-etl-gitcommits"
responseFromContainerInfo(dockerClientProvider.client.inspectContainer(containerName))
}
private def responseFromContainerInfo(containerInfo: ContainerInfo) = {
containerInfo.state match {
case s if s.exitCode != 0 =>
throw new RestApiException(s"Data import failed")
case s if s.running =>
Response.withStatusCode(202, "processing")
case s if s.status == "exited" =>
//Spark ETL job exited successfully
Response.withStatusCode(204, "finished")
case _ => throw new RestApiException(s"Case not handled")
}
}
}
object AnalyticsWizardActions {
// URLEncoder could potentially throw UnsupportedEncodingException,
// but UTF-8 will *always* be resolved, otherwise, Gerrit wouldn't work at all
def encodedName(name: String) =
try {
URLEncoder.encode(name, "UTF-8")
} catch {
case e: Throwable => throw new RuntimeException(e)
}
}
@ImplementedBy(classOf[DockerClientProviderImpl])
trait DockerClientProvider {
def client: DockerClient
}
class DockerClientProviderImpl extends DockerClientProvider {
def client: DockerClient = DefaultDockerClient.fromEnv.build
}