Skip to content

Commit

Permalink
Hipp 1841 revisit hip environment config (#251)
Browse files Browse the repository at this point in the history
* Spike into evolved HipEnvironments

* IT stability

* HIPP-1841: revisit hip environment config

* HIPP-1841: Fix merge

* HIPP-1841: Tidy up

* HIPP-1841: Tidy up

* HIPP-1841: Tidy up

---------

Co-authored-by: PaulCDurham <23658502+PaulCDurham@users.noreply.github.com>
  • Loading branch information
llmikeyj and PaulCDurham authored Feb 10, 2025
1 parent 95067e4 commit 6f48b35
Show file tree
Hide file tree
Showing 10 changed files with 732 additions and 120 deletions.
296 changes: 254 additions & 42 deletions app/uk/gov/hmrc/apihubapplications/config/HipEnvironments.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,32 @@

package uk.gov.hmrc.apihubapplications.config

import com.ctc.wstx.util.URLUtil
import com.google.inject.{Inject, Singleton}
import com.typesafe.config.Config
import play.api.{ConfigLoader, Configuration}
import java.net.URL
import scala.annotation.tailrec
import scala.util.Try

case class HipEnvironment(
// Define the attributes of an environment in an abstract way
trait AbstractHipEnvironment[T] {
def id: String
def rank: Int
def isProductionLike: Boolean
def apimUrl: String
def clientId: String
def secret: String
def useProxy: Boolean
def apiKey: Option[String]
def promoteTo: Option[T]
def apimEnvironmentName: String
}

// This can be serialised/de-serialised to support REST calls
// It doesn't have promoteTo as Option[HipEnvironment] which isn't nice to code with
// Use this as the input to HipEnvironments and to pass from BE to FE
case class BaseHipEnvironment(
id: String,
rank: Int,
isProductionLike: Boolean,
Expand All @@ -29,72 +50,263 @@ case class HipEnvironment(
secret: String,
useProxy: Boolean,
apiKey: Option[String],
promoteTo: Option[String],
apimEnvironmentName: String
)

object HipEnvironment {

implicit val hipEnvironmentConfigLoader: ConfigLoader[HipEnvironment] =
(rootConfig: Config, path: String) => {
val config = rootConfig.getConfig(path)

HipEnvironment(
id = config.getString("id"),
rank = config.getInt("rank"),
isProductionLike = config.getBoolean("isProductionLike"),
apimUrl = config.getString("apimUrl"),
clientId = config.getString("clientId"),
secret = config.getString("secret"),
useProxy = config.getBoolean("useProxy"),
apiKey = getOptionalString(config, "apiKey"),
apimEnvironmentName = config.getString("apimEnvironmentName")
)
}
) extends AbstractHipEnvironment[String]

private def getOptionalString(config: Config, path: String): Option[String] = {
if (config.hasPath(path)) {
Some(config.getString(path))
}
else {
None
}
}
// Nicer, as promoteTo now gives us a HipEnvironment
// Use this everywhere in code
// We want to do some hooky lazy val stuff below so a trait is good
trait HipEnvironment extends AbstractHipEnvironment[HipEnvironment]

}
// Default implementation for use in fakes etc
case class DefaultHipEnvironment(
id: String,
rank: Int,
isProductionLike: Boolean,
apimUrl: String,
clientId: String,
secret: String,
useProxy: Boolean,
apiKey: Option[String],
promoteTo: Option[HipEnvironment],
apimEnvironmentName: String ) extends HipEnvironment

trait HipEnvironments {

def environments: Seq[HipEnvironment]
protected def baseEnvironments: Seq[BaseHipEnvironment]

def productionEnvironment: HipEnvironment
def environments: Seq[HipEnvironment] = {
baseEnvironments
.map(
base =>
new Object with HipEnvironment:
override val id: String = base.id
override val rank: Int = base.rank
override val isProductionLike: Boolean = base.isProductionLike
override val apimUrl: String = base.apimUrl
override val clientId: String = base.clientId
override val secret: String = base.secret
override val useProxy: Boolean = base.useProxy
override val apiKey: Option[String] = base.apiKey
override lazy val promoteTo: Option[HipEnvironment] = base.promoteTo.map(forId)
override val apimEnvironmentName: String = base.apimEnvironmentName
)
.sortBy(_.rank)
}

def deploymentEnvironment: HipEnvironment
def production: HipEnvironment

def deployTo: HipEnvironment

def validateIn: HipEnvironment

def forId(environmentId: String): HipEnvironment = {
environments
.find(_.id == environmentId)
.getOrElse(throw new IllegalArgumentException(s"No configuration for environment $environmentId"))
}

def forUrlPathParameter(pathParameter: String): Option[HipEnvironment] =
def forUrlPathParameter(pathParameter: String): Option[HipEnvironment] =
environments.find(hipEnvironment => hipEnvironment.id == pathParameter)

}

// In the frontend we would have a RestHipEnvironmentsImpl
// This would need a lazy baseEnvironments that would be fetched on demand from the BE
@Singleton
class HipEnvironmentsImpl @Inject(configuration: Configuration) extends HipEnvironments {
class ConfigurationHipEnvironmentsImpl @Inject(configuration: Configuration) extends HipEnvironments {

import ConfigurationHipEnvironmentsImpl._

private val baseConfig = buildBaseConfig(configuration)

override protected val baseEnvironments: Seq[BaseHipEnvironment] = {
buildEnvironments(baseConfig)
}

override def production: HipEnvironment = environments.find(_.id == baseConfig.production).head

override def deployTo: HipEnvironment = environments.find(_.id == baseConfig.deployTo).head

override def validateIn: HipEnvironment = environments.find(_.id == baseConfig.validateIn).head

}

object ConfigurationHipEnvironmentsImpl {

case class ConfigHipEnvironment(
id: String,
rank: Int,
apimUrl: String,
clientId: String,
secret: String,
useProxy: Boolean,
apiKey: Option[String],
promoteTo: Option[String],
apimEnvironmentName: String
)

object ConfigHipEnvironment {

implicit val hipEnvironmentConfigLoader: ConfigLoader[ConfigHipEnvironment] =
(rootConfig: Config, path: String) => {
val config = rootConfig.getConfig(path)

ConfigHipEnvironment(
id = config.getString("id"),
rank = config.getInt("rank"),
apimUrl = config.getString("apimUrl"),
clientId = config.getString("clientId"),
secret = config.getString("secret"),
useProxy = config.getBoolean("useProxy"),
apiKey = getOptionalString(config, "apiKey"),
promoteTo = getOptionalString(config, "promoteTo"),
apimEnvironmentName = config.getString("apimEnvironmentName")
)
}

private def getOptionalString(config: Config, path: String): Option[String] = {
if (config.hasPath(path)) {
Some(config.getString(path))
}
else {
None
}
}

override val environments: Seq[HipEnvironment] = {
configuration
.get[Map[String, HipEnvironment]]("hipEnvironments")
}

case class BaseConfig(
environments: Seq[ConfigHipEnvironment],
production: String,
deployTo: String,
validateIn: String
)

def buildBaseConfig(configuration: Configuration): BaseConfig = {
val environments = configuration
.get[Map[String, ConfigHipEnvironment]]("hipEnvironments.environments")
.values
.toSeq
.sortBy(_.rank)

val production = configuration.get[String]("hipEnvironments.production")
val deployTo = configuration.get[String]("hipEnvironments.deployTo")
val validateIn = configuration.get[String]("hipEnvironments.validateIn")

BaseConfig(environments, production, deployTo, validateIn)
}

def buildEnvironments(baseConfig: BaseConfig): Seq[BaseHipEnvironment] = {
validate(baseConfig)

baseConfig.environments
.map(
base => BaseHipEnvironment(
id = base.id,
rank = base.rank,
isProductionLike = base.id == baseConfig.production,
apimUrl = base.apimUrl,
clientId = base.clientId,
secret = base.secret,
useProxy = base.useProxy,
apiKey = base.apiKey,
promoteTo = base.promoteTo,
apimEnvironmentName = base.apimEnvironmentName
)

)
}

private def validate(baseConfig: BaseConfig): Unit = {
validateRanks(baseConfig.environments)
validateIds(baseConfig.environments)
validateProduction(baseConfig)
validateDeployTo(baseConfig)
validateValidateIn(baseConfig)
validatePromoteTos(baseConfig)
validateApimUrls(baseConfig.environments)
validateApiKeys(baseConfig.environments)
}

private def validateRanks(environments: Seq[ConfigHipEnvironment]): Unit = {
// ranks mandatory, start at 1, be contiguous,
val actualRanks = environments.map(_.rank).toSet
val expectedRanks = Seq.range(1, environments.size + 1).toSet
if (!actualRanks.equals(expectedRanks)) {
throw new IllegalArgumentException("Hip environments must have valid ranks.")
}
}

private def validateIds(environments: Seq[ConfigHipEnvironment]): Unit = {
// ids must be unique and URL safe
val actualIds = environments.map(_.id).toSet
if (!actualIds.size.equals(environments.size)) {
throw new IllegalArgumentException("Hip environment ids must be unique.")
}
if (actualIds.exists(id => !id.matches("[A-Za-z0-9-_.~]+"))) {
throw new IllegalArgumentException("Hip environment ids must only contain URL unreserved characters.")
}
}

private def validateProduction(baseConfig: BaseConfig): Unit = {
// production id must be real, and production cannot promote
baseConfig.environments.find(_.id == baseConfig.production).getOrElse(throw new IllegalArgumentException(s"production id ${baseConfig.production} must match one of the configured environments."))
if (baseConfig.environments.find(_.id == baseConfig.production).exists(_.promoteTo.isDefined)) {
throw new IllegalArgumentException(s"production environment cannot promote to anywhere.")
}
}

private def validateDeployTo(baseConfig: BaseConfig): Unit = {
// deployTo must be real
baseConfig.environments.find(_.id == baseConfig.deployTo).getOrElse(throw new IllegalArgumentException(s"deployTo id ${baseConfig.deployTo} must match one of the configured environments."))
}

private def validateValidateIn(baseConfig: BaseConfig): Unit = {
// validateIn must be real
baseConfig.environments.find(_.id == baseConfig.validateIn).getOrElse(throw new IllegalArgumentException(s"validateIn id ${baseConfig.validateIn} must match one of the configured environments."))
}

override val productionEnvironment: HipEnvironment = environments.find(_.isProductionLike)
.getOrElse(throw new IllegalArgumentException("No production environment configured"))
private def validatePromoteTos(baseConfig: BaseConfig): Unit = {
// promoteTo must be real, unique, and not cyclical
val actualIds = baseConfig.environments.map(_.id).toSet
val actualPromoteTos = baseConfig.environments.flatMap(_.promoteTo)
val actualPromoteTosSet = actualPromoteTos.toSet

override val deploymentEnvironment: HipEnvironment = environments.maxBy(_.rank)
if (!actualPromoteTos.size.equals(actualPromoteTosSet.size)) {
throw new IllegalArgumentException("promoteTo ids must be unique.")
}

if (!actualPromoteTosSet.subsetOf(actualIds)) {
throw new IllegalArgumentException("promoteTo ids must be real.")
}

baseConfig.environments.foreach(env => {
val thisEnvId = env.id
var promoteTo = env.promoteTo
while (promoteTo.isDefined) {
if (promoteTo.get.equals(thisEnvId)) {
throw new IllegalArgumentException("environments cannot cyclically promoteTo themselves.")
}
promoteTo = baseConfig.environments.find(_.id == promoteTo.get).flatMap(_.promoteTo)
}
})
}

private def validateApimUrls(environments: Seq[ConfigHipEnvironment]): Unit = {
// must be valid url
environments.foreach(env => {
val value = Try(new URL(env.apimUrl)).toOption
if (value.isEmpty) {
throw new IllegalArgumentException("environments must have a valid apimUrl.")
}
})
}

private def validateApiKeys(environments: Seq[ConfigHipEnvironment]): Unit = {
// environments with useProxy must have apiKey
if (environments.filter(_.useProxy).exists(_.apiKey.isEmpty)) {
throw new IllegalArgumentException("environments with useProxy=true must have an apiKey.")
}
}
}
2 changes: 1 addition & 1 deletion app/uk/gov/hmrc/apihubapplications/config/Module.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Module extends play.api.inject.Module {
bindz(classOf[ApplicationsCredentialsService]).to(classOf[ApplicationsCredentialsServiceImpl]).eagerly(),
bindz(classOf[ApplicationsLifecycleService]).to(classOf[ApplicationsLifecycleServiceImpl]).eagerly(),
bindz(classOf[ApplicationsSearchService]).to(classOf[ApplicationsSearchServiceImpl]).eagerly(),
bindz(classOf[HipEnvironments]).to(classOf[HipEnvironmentsImpl]).eagerly(),
bindz(classOf[HipEnvironments]).to(classOf[ConfigurationHipEnvironmentsImpl]).eagerly(),
bindz(classOf[HipEnvironmentActionProvider]).to(classOf[HipEnvironmentActionProviderImpl]).eagerly(),
bindz(classOf[AutopublishConnector]).to(classOf[AutopublishConnectorImpl]).eagerly()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class APIMConnectorImpl @Inject()(
import CorrelationIdSupport.*

override def validateInPrimary(oas: String)(implicit hc: HeaderCarrier): Future[Either[ApimException, ValidateResponse]] = {
val hipEnvironment = hipEnvironments.productionEnvironment
val hipEnvironment = hipEnvironments.production

httpClient.post(url"${hipEnvironment.apimUrl}/v1/simple-api-deployment/validate")
.setHeader("Authorization" -> authorizationForEnvironment(hipEnvironment))
Expand All @@ -76,7 +76,7 @@ class APIMConnectorImpl @Inject()(
}

override def deployToSecondary(request: DeploymentsRequest)(implicit hc: HeaderCarrier): Future[Either[ApimException, DeploymentsResponse]] = {
val hipEnvironment = hipEnvironments.deploymentEnvironment
val hipEnvironment = hipEnvironments.deployTo

val metadata = Json.toJson(CreateMetadata(request))
val context = Seq("metadata" -> Json.prettyPrint(metadata))
Expand Down Expand Up @@ -111,7 +111,7 @@ class APIMConnectorImpl @Inject()(
}

override def redeployToSecondary(publisherReference: String, request: RedeploymentRequest)(implicit hc: HeaderCarrier): Future[Either[ApimException, DeploymentsResponse]] = {
val hipEnvironment = hipEnvironments.deploymentEnvironment
val hipEnvironment = hipEnvironments.deployTo

val metadata = Json.toJson(UpdateMetadata(request))
val context = Seq("publisherReference" -> publisherReference, "metadata" -> Json.prettyPrint(metadata))
Expand Down Expand Up @@ -169,7 +169,7 @@ class APIMConnectorImpl @Inject()(
}

override def getDeploymentDetails(publisherReference: String)(implicit hc: HeaderCarrier): Future[Either[ApimException, DeploymentDetails]] = {
val hipEnvironment = hipEnvironments.deploymentEnvironment
val hipEnvironment = hipEnvironments.deployTo

val context = Seq("publisherReference" -> publisherReference)
.withCorrelationId()
Expand Down Expand Up @@ -246,7 +246,7 @@ class APIMConnectorImpl @Inject()(
}
}

override def listEgressGateways(hipEnvironment: HipEnvironment)(implicit hc: HeaderCarrier): Future[Either[ApimException, Seq[EgressGateway]]] = {
def listEgressGateways(hipEnvironment: HipEnvironment)(implicit hc: HeaderCarrier): Future[Either[ApimException, Seq[EgressGateway]]] = {
val context = Seq.empty.withCorrelationId()

httpClient.get(url"${hipEnvironment.apimUrl}/v1/simple-api-deployment/egress-gateways")
Expand Down
Loading

0 comments on commit 6f48b35

Please # to comment.