Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HIPP-1630: Add code for circuit breaker/synthetic monitoring spike #433

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/config/Module.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Module extends play.api.inject.Module {
bindz[AddAnApiCheckContextActionProvider].to(classOf[AddAnApiCheckContextActionProviderImpl]).eagerly(),
bindz(classOf[IdentifierAction]).to(classOf[AuthenticatedIdentifierAction]).eagerly(),
bindz(classOf[OptionalIdentifierAction]).to(classOf[OptionallyAuthenticatedIdentifierAction]),
bindz[StatusActionProvider].to(classOf[StatusActionProviderImpl]).eagerly(),
bindz(classOf[Clock]).toInstance(Clock.systemDefaultZone.withZone(ZoneOffset.UTC)),
bindz[Encrypter & Decrypter].toProvider[CryptoProvider],
bindz[Domains].to(classOf[DomainsImpl]).eagerly(),
Expand Down
8 changes: 8 additions & 0 deletions app/connectors/ApplicationsConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import models.deployment.*
import models.exception.{ApplicationCredentialLimitException, ApplicationsException, TeamNameNotUniqueException}
import models.requests.{AddApiRequest, ChangeTeamNameRequest, TeamMemberRequest}
import models.stats.ApisInProductionStatistic
import models.status.ServiceStatuses
import models.team.{NewTeam, Team}
import models.user.UserContactDetails
import play.api.Logging
Expand Down Expand Up @@ -598,4 +599,11 @@ class ApplicationsConnector @Inject()(
.setHeader(AUTHORIZATION -> clientAuthToken)
.execute[Seq[EgressGateway]]
}

def status()(implicit hc: HeaderCarrier): Future[ServiceStatuses] = {
httpClient.get(url"$applicationsBaseUrl/api-hub-applications/status")
.setHeader(ACCEPT -> JSON)
.setHeader(AUTHORIZATION -> clientAuthToken)
.execute[ServiceStatuses]
}
}
10 changes: 6 additions & 4 deletions app/controllers/IndexController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package controllers

import controllers.actions.IdentifierAction
import controllers.actions.{IdentifierAction, StatusActionProvider}
import play.api.Logging
import play.api.i18n.I18nSupport
import play.api.mvc._
import play.api.mvc.*
import services.ApiHubService
import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController
import views.html.IndexView
Expand All @@ -30,11 +30,12 @@ import scala.concurrent.ExecutionContext
class IndexController @Inject()(
val controllerComponents: MessagesControllerComponents,
identify: IdentifierAction,
status: StatusActionProvider,
view: IndexView,
apiHubService: ApiHubService
)(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport with Logging {

def onPageLoad: Action[AnyContent] = identify.async { implicit request =>
def onPageLoad: Action[AnyContent] = (identify andThen status()).async { implicit request =>
val maxApplicationsToShow = 5
val maxTeamsToShow = 5

Expand All @@ -46,7 +47,8 @@ class IndexController @Inject()(
userApps.size,
userTeams.sortBy(_.created).reverse.take(maxTeamsToShow),
userTeams.size,
Some(request.user)
Some(request.user),
request.serviceStatuses
))
}

Expand Down
53 changes: 53 additions & 0 deletions app/controllers/actions/StatusAction.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2023 HM Revenue & Customs
*
* 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.actions

import com.google.inject.Inject
import connectors.ApplicationsConnector
import handlers.ErrorHandler
import models.requests.{ApiRequest, IdentifierRequest, StatusRequest}
import models.user.{LdapUser, StrideUser, UserModel, UserType}
import play.api.Logging
import play.api.mvc.Results.*
import play.api.mvc.*
import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendHeaderCarrierProvider

import scala.concurrent.{ExecutionContext, Future}

trait StatusAction extends ActionRefiner[IdentifierRequest, StatusRequest]

trait StatusActionProvider {
def apply()(implicit ec: ExecutionContext): StatusAction
}

class StatusActionProviderImpl @Inject()(
applicationsConnector: ApplicationsConnector,
errorHandler: ErrorHandler
)(implicit val executionContext: ExecutionContext) extends StatusActionProvider {

def apply()(implicit ec: ExecutionContext): StatusAction =
new StatusAction with FrontendHeaderCarrierProvider {
override protected def refine[A](identifierRequest: IdentifierRequest[A]): Future[Either[Result, StatusRequest[A]]] =
implicit val request: Request[?] = identifierRequest
applicationsConnector.status().map(statuses =>
Right(StatusRequest(identifierRequest, identifierRequest.user, statuses))
)

override protected def executionContext: ExecutionContext = ec
}

}
23 changes: 23 additions & 0 deletions app/models/requests/StatusRequest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2023 HM Revenue & Customs
*
* 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 models.requests

import models.status.ServiceStatuses
import models.user.UserModel
import play.api.mvc.WrappedRequest

case class StatusRequest[A](identifierRequest: IdentifierRequest[A], user: UserModel, serviceStatuses: ServiceStatuses) extends WrappedRequest[A](identifierRequest)
31 changes: 31 additions & 0 deletions app/models/status/ServiceStatus.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2024 HM Revenue & Customs
*
* 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 models.status

import models.application.EnvironmentName
import play.api.libs.json.{Format, Json}

case class ServiceStatus(isDown: Boolean, service: String, environmentName: EnvironmentName)
object ServiceStatus:
given statusFormat: Format[ServiceStatus] = Json.format[ServiceStatus]

case class ServiceStatuses(statuses: Seq[ServiceStatus]) {
lazy val hasServiceDown: Boolean = statuses.exists(_.isDown)
}
object ServiceStatuses:
given statusesFormat: Format[ServiceStatuses] = Json.format[ServiceStatuses]

6 changes: 4 additions & 2 deletions app/views/IndexView.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*@

@import models.application.Application
@import models.status.ServiceStatuses
@import models.user.UserModel
@import views.ViewUtils
@import models.team.Team
Expand All @@ -26,14 +27,15 @@
govukTable: GovukTable
)

@(applications: Seq[Application], totalApplicationCount: Int, teams: Seq[Team], totalTeamCount: Int, user: Option[UserModel])(implicit request: Request[?], messages: Messages)
@(applications: Seq[Application], totalApplicationCount: Int, teams: Seq[Team], totalTeamCount: Int, user: Option[UserModel], statuses: ServiceStatuses)(implicit request: Request[?], messages: Messages)

@layout(
pageTitle = titleNoForm(messages("dashboard.title")),
showBackLink = true,
fullWidth = true,
user = user,
activeLink = Some("dashboard")
activeLink = Some("dashboard"),
statuses = Some(statuses)
) {
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
Expand Down
27 changes: 25 additions & 2 deletions app/views/templates/Layout.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

@import components.FullWidthMainContent
@import config.FrontendAppConfig
@import models.status.ServiceStatuses
@import models.user.{StrideUser, UserModel}
@import templates.ApiHubHeader
@import uk.gov.hmrc.hmrcfrontend.views.config.StandardAlphaBanner
Expand All @@ -37,7 +38,8 @@
twoThirdsMainContent: TwoThirdsMainContent,
fullWidthMainContentLayout: FullWidthMainContent,
header: ApiHubHeader,
hmrcInternalHead: HmrcInternalHead
hmrcInternalHead: HmrcInternalHead,
govukPhaseBanner: GovukPhaseBanner
)

@(
Expand All @@ -49,7 +51,8 @@
fullWidth: Boolean = false,
customScriptsBlock: Option[Html] = None,
customStyles: Option[Html] = None,
activeLink: Option[String] = None
activeLink: Option[String] = None,
statuses: Option[ServiceStatuses] = None,
)(contentBlock: Html)(implicit request: RequestHeader, messages: Messages)

@head = {
Expand Down Expand Up @@ -103,6 +106,26 @@
@messages("site.feedback")
</a>

@for(serviceStatuses <- statuses) {
@govukPhaseBanner(PhaseBanner(
tag = Some(Tag(
content = Text(
if (serviceStatuses.hasServiceDown) then
"Ongoing issues"
else
"Good service"
),
classes = s"${if (serviceStatuses.hasServiceDown) then "govuk-tag--red" else ""}"
)),
content = Text(
if (serviceStatuses.hasServiceDown) then
"We are experiencing some issues."
else
"All services are healthy."
)
))
}

@contentBlock

<div class="govuk-!-margin-top-8">
Expand Down
5 changes: 3 additions & 2 deletions test/controllers/IndexControllerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import controllers.IndexControllerSpec.buildFixture
import controllers.actions.FakeUser
import generators.TeamGenerator
import models.application.{Application, Creator, TeamMember}
import models.status.ServiceStatuses
import org.mockito.ArgumentMatchers.{any, eq => eqTo}
import org.mockito.Mockito.when
import org.scalatestplus.mockito.MockitoSugar
Expand Down Expand Up @@ -66,7 +67,7 @@ class IndexControllerSpec extends SpecBase with MockitoSugar with TeamGenerator

val sortedApplications = applications.sortBy(_.created).reverse
val sortedTeams = teams.sortBy(_.created).reverse
contentAsString(result) mustEqual view(sortedApplications, applications.size, sortedTeams, teams.size, Some(FakeUser))(request, messages(fixture.application)).toString
contentAsString(result) mustEqual view(sortedApplications, applications.size, sortedTeams, teams.size, Some(FakeUser), ServiceStatuses(Seq.empty))(request, messages(fixture.application)).toString
contentAsString(result) must validateAsHtml
}
}
Expand All @@ -93,7 +94,7 @@ class IndexControllerSpec extends SpecBase with MockitoSugar with TeamGenerator

status(result) mustEqual OK

contentAsString(result) mustEqual view(applications, 0, teams, 0, Some(FakeUser))(request, messages(fixture.application)).toString
contentAsString(result) mustEqual view(applications, 0, teams, 0, Some(FakeUser), ServiceStatuses(Seq.empty))(request, messages(fixture.application)).toString
contentAsString(result) must validateAsHtml
}
}
Expand Down