riff-raff/app/resources/PrismLookup.scala (222 lines of code) (raw):

package resources import conf.Config import controllers.Logging import magenta._ import org.joda.time.format.{DateTimeFormat, DateTimeFormatter} import org.joda.time.{DateTime, DateTimeZone} import play.api.libs.functional.syntax._ import play.api.libs.json._ import play.api.libs.ws.WSClient import utils.Json._ import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.util.control.NonFatal import scala.util.{Failure, Success, Try} case class Image( imageId: String, creationDate: DateTime, tags: Map[String, String] ) object Image { implicit val formats: OFormat[Image] = Json.format[Image] } class PrismLookup( config: Config, wsClient: WSClient, secretProvider: SecretProvider ) extends Lookup with Logging { def keyRing(stage: Stage, app: App, stack: Stack): KeyRing = { val KeyPattern = """credentials:(.*)""".r val apiCredentials = data.keys flatMap { case key @ KeyPattern(service) => data.datum(key, app, stage, stack).flatMap { data => if (service.endsWith("role")) { Some( service -> ApiRoleCredentials(service, data.value, data.comment) ) } else { secretProvider.lookup(service, data.value).map { secret => service -> ApiStaticCredentials( service, data.value, secret, data.comment ) } } } case _ => None } KeyRing(apiCredentials.distinct.toMap) } object prism extends Logging { def get[T](path: String, retriesLeft: Int = 5)(block: JsValue => T): T = { val result = wsClient.url(s"${config.lookup.prismUrl}$path").get().map(_.json).map { json => block(json) } try { Await.result(result, config.lookup.timeoutSeconds.seconds) } catch { case NonFatal(e) => log.warn( s"Call to prism failed ($path; $retriesLeft retries left)", e ) if (retriesLeft > 0) get(path, retriesLeft - 1)(block) else throw e } } } val formatter: DateTimeFormatter = DateTimeFormat.forPattern("EEE MMM dd HH:mm:ss 'UTC' yyyy") implicit val datumReads = Json.reads[Datum] implicit val hostReads = ( (__ \ "dnsName").read[String] and (__ \ "stack").read[String] and (__ \ "app").read[Seq[String]] and (__ \ "stage").read[String] and (__ \ "group").read[String] and (__ \ "createdAt").read[DateTime] and (__ \ "instanceName").readNullable[String] and (__ \ "internalName").readNullable[String] and (__ \ "dnsName").read[String] ) { ( name: String, stack: String, appList: Seq[String], stage: String, group: String, createdAt: DateTime, instanceName: Option[String], internalName: Option[String], dnsName: String ) => val app: App = App(appList.head) val tags = { Map( "group" -> group, "created_at" -> formatter.print( createdAt.toDateTime(DateTimeZone.UTC) ), "dnsname" -> dnsName ) ++ instanceName.map("instancename" ->) ++ internalName.map("internalname" ->) } Host( name = name, app = app, stage = stage, stack = stack, tags = tags ) } implicit val dataReads = ( (__ \ "key").read[String] and (__ \ "values").read[Seq[Datum]] ) tupled def name = "Prism" def lastUpdated: DateTime = prism.get("/sources?resource=instance") { json => val sourceCreatedAt = json \ "data" match { case JsDefined(JsArray(sources)) => sources.map { source => (source \ "state" \ "createdAt").as[DateTime] } case _ => Seq(new DateTime(0)) } sourceCreatedAt.minBy(_.getMillis) } def data = new DataLookup { def keys: Seq[String] = prism.get("/data/keys") { json => (json \ "data" \ "keys").as[Seq[String]] } def all: Map[String, Seq[Datum]] = prism.get("/data?_expand") { json => (json \ "data" \ "data").as[Seq[(String, Seq[Datum])]].toMap } def datum( key: String, app: App, stage: Stage, stack: Stack ): Option[Datum] = { val query = s"/data/lookup/${key.urlEncode}?stack=${stack.name.urlEncode}&app=${app.name.urlEncode}&stage=${stage.name.urlEncode}" prism.get(query) { json => (json \ "data").asOpt[Datum] } } } def hosts = new HostLookup { def parseHosts(json: JsValue, entity: String = "instances"): Seq[Host] = { val tryHosts = (json \ "data" \ entity).as[JsArray].value.map { jsHost => Try(jsHost.as[Host]) } val errors = tryHosts.flatMap { case f @ Failure(e) => Some(f) case _ => None } if (errors.nonEmpty) log.warn( s"Encountered ${errors.size} (of ${tryHosts.size}) $entity records that could not be parsed in Prism response" ) if (log.isDebugEnabled) errors.foreach(e => log.debug("Couldn't parse instance from Prism data", e.exception) ) tryHosts.toSeq.flatMap { case Success(hosts) => Some(hosts) case _ => None } } def get( pkg: DeploymentPackage, app: App, parameters: DeployParameters, stack: Stack, entity: String ): Seq[Host] = { val query = s"/$entity?_expand&stage=${parameters.stage.name.urlEncode}&stack=${stack.name.urlEncode}&app=${app.name.urlEncode}" prism.get(query)(js => parseHosts(js, entity)) } def get( pkg: DeploymentPackage, app: App, parameters: DeployParameters, stack: Stack ): Seq[Host] = { get(pkg, app, parameters, stack, "instances") } def all: Seq[Host] = prism.get("/instances?_expand")(js => parseHosts(js, "instances")) } def stages: Seq[String] = prism.get("/stages") { json => (json \ "data" \ "stages").as[Seq[String]] } private def get( accountNumber: Option[String], region: String, tags: Map[String, String] ): Seq[Image] = { val params: Seq[(String, String)] = tags.map { case (key, value) => s"tags.${key.urlEncode}" -> value }.toSeq ++ accountNumber.map(acc => "meta.origin.accountNumber" -> acc) ++ Map("region" -> region, "state" -> "available") val paramsQueryString = params.map { case (k, v) => s"$k=${v.urlEncode}" }.mkString("&") prism.get(s"/images?$paramsQueryString") { json => (json \ "data" \ "images").as[Seq[Image]] } } def getLatestAmi( accountNumber: Option[String], tagFilter: Map[String, String] => Boolean )(region: String)(tags: Map[String, String]): Option[String] = get(accountNumber, region, tags) .filter(image => tagFilter(image.tags)) .sortBy(-_.creationDate.getMillis) .headOption .map(_.imageId) }