admin/app/controllers/admin/TroubleshooterController.scala (180 lines of code) (raw):

package controllers.admin import com.amazonaws.services.ec2.model.{DescribeInstancesRequest, Filter} import com.amazonaws.services.ec2.{AmazonEC2, AmazonEC2ClientBuilder} import common.{GuLogging, ImplicitControllerExecutionContext} import conf.Configuration.aws.credentials import contentapi.{CapiHttpClient, ContentApiClient, PreviewContentApi, PreviewSigner} import model.{ApplicationContext, NoCache} import play.api.Mode import play.api.libs.ws.WSClient import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} import tools.LoadBalancer import scala.jdk.CollectionConverters._ import scala.concurrent.Future import scala.concurrent.duration._ import scala.util.Random case class EndpointStatus(name: String, isOk: Boolean, messages: String*) object TestPassed { def apply(name: String): EndpointStatus = EndpointStatus(name, true) } object TestFailed { def apply(name: String, messages: String*): EndpointStatus = EndpointStatus(name, false, messages: _*) } class TroubleshooterController(wsClient: WSClient, val controllerComponents: ControllerComponents)(implicit appContext: ApplicationContext, ) extends BaseController with GuLogging with ImplicitControllerExecutionContext { private val capiLiveHttpClient = new CapiHttpClient(wsClient) private val capiPreviewHttpClient = new CapiHttpClient(wsClient) { override val signer = Some(PreviewSigner()) } val contentApi = new ContentApiClient(capiLiveHttpClient) val previewContentApi = new PreviewContentApi(capiPreviewHttpClient) private lazy val awsEc2Client: Option[AmazonEC2] = credentials.map { credentials => AmazonEC2ClientBuilder .standard() .withCredentials(credentials) .withRegion(conf.Configuration.aws.region) .build() } def index(): Action[AnyContent] = Action { implicit request => NoCache(Ok(views.html.troubleshooter(LoadBalancer.all.filter(_.testPath.isDefined)))) } def test(id: String, testPath: String): Action[AnyContent] = Action.async { implicit request => val pathToTest = if (testPath.startsWith("/")) testPath else s"/$testPath" // appending leading '/' if user forgot to include it val loadBalancers = LoadBalancer.all.filter(_.testPath.isDefined) val thisLoadBalancer = loadBalancers.find(_.project == id) val directToLoadBalancer = thisLoadBalancer .map(testOnLoadBalancer(_, pathToTest, id)) .getOrElse(Future.successful(TestFailed("Can find the appropriate loadbalancer"))) val viaWebsite = testOnGuardianSite(pathToTest, id) val directToContentApi = testOnContentApi(pathToTest, id) val directToRouter = testOnRouter(pathToTest, id) val directToPreviewContentApi = testOnPreviewContentApi(pathToTest, id) val viaPreviewWebsite = testOnPreviewSite(pathToTest, id) // NOTE - the order of these is important, they are ordered so that the first failure is furthest 'back' // in the stack Future .sequence( Seq( directToContentApi, directToLoadBalancer, directToRouter, viaWebsite, directToPreviewContentApi, viaPreviewWebsite, ), ) .map { results => NoCache(Ok(views.html.troubleshooterResults(thisLoadBalancer, results))) } } private def testOnRouter(testPath: String, id: String): Future[EndpointStatus] = { def fetchWithRouterUrl(url: String) = { val result = httpGet("Can fetch directly from Router load balancer", s"http://$url$testPath") result.map { result => if (result.isOk) result else TestFailed( result.name, result.messages :+ "NOTE: if hitting the Router you MUST set Host header to 'www.theguardian.com' or else you will get '403 Forbidden'": _*, ) } } val routerUrl = if (appContext.environment.mode == Mode.Prod) { // Workaround in PROD: // Getting the private dns of one of the router instances because // the Router ELB can only be accessed via its public IP/DNS from Fastly or Guardian VPN/office, not from an Admin instance // However Admin instances can access router instances via private IPs // This is of course not very fast since it has to make a call to AWS API before to fetch the url // but the troubleshooter is an admin only tool val tagsAsFilters = Map( "Stack" -> "frontend", "App" -> "router", "Stage" -> "PROD", ).map { case (name, value) => new Filter("tag:" + name).withValues(value) }.asJavaCollection val instancesDnsName: Seq[String] = awsEc2Client .map( _.describeInstances(new DescribeInstancesRequest().withFilters(tagsAsFilters)).getReservations.asScala .flatMap(_.getInstances.asScala) .map(_.getPrivateDnsName), ) .toSeq .flatten Random.shuffle(instancesDnsName).headOption } else { LoadBalancer("frontend-router").flatMap(_.url) } routerUrl .map(fetchWithRouterUrl) .getOrElse(Future.successful(TestFailed("Can get Frontend router url"))) } private def testOnLoadBalancer( thisLoadBalancer: LoadBalancer, testPath: String, id: String, ): Future[EndpointStatus] = { thisLoadBalancer.url .map { url => httpGet(s"Can fetch directly from ${thisLoadBalancer.name} load balancer", s"http://$url$testPath") } .getOrElse(Future(TestFailed(s"Can get ${thisLoadBalancer.name}'s loadbalancer url"))) } private def testOnContentApi(testPath: String, id: String): Future[EndpointStatus] = { val testName = "Can fetch directly from Content API" val request = contentApi.item(testPath, "UK").showFields("all") contentApi .getResponse(request) .map { response => if (response.status == "ok") { TestPassed(testName) } else { TestFailed(testName, request.toString) } } .recoverWith { case t: Throwable => Future.successful(TestFailed("Direct to content api", t.getMessage, request.toString)) } } private def testOnPreviewContentApi(testPath: String, id: String): Future[EndpointStatus] = { val testName = "Can fetch directly from Preview Content API" val request = previewContentApi.item(testPath, "UK").showFields("all") previewContentApi .getResponse(request) .map { response => if (response.status == "ok") { TestPassed(testName) } else { TestFailed(testName, request.toString) } } .recoverWith { case t: Throwable => Future.successful(TestFailed("Direct to Preview Content API", t.getMessage, request.toString)) } } private def testOnGuardianSite(testPath: String, id: String): Future[EndpointStatus] = { httpGet("Can fetch from www.theguardian.com", s"https://www.theguardian.com$testPath") } private def testOnPreviewSite(testPath: String, id: String): Future[EndpointStatus] = { httpGet( "Can fetch from preview.gutools.co.uk", s"https://preview.gutools.co.uk$testPath", Some("preview.gutools.co.uk"), ) } private def httpGet(testName: String, url: String, virtualHost: Option[String] = Some("www.theguardian.com")) = { wsClient .url(url) .withVirtualHost(virtualHost.getOrElse("")) .withRequestTimeout(5.seconds) .get() .map { response => if (response.status == 200) { TestPassed(testName) } else { TestFailed(testName, s"Status: ${response.status}", url) } } .recoverWith { case t: Throwable => Future.successful(TestFailed(testName, t.getMessage, url)) } } }