app/agent/model.scala (141 lines of code) (raw):
package agent
import org.joda.time.{DateTime, DateTimeZone, Duration => JodaDuration}
import scala.util.Try
import scala.util.control.NonFatal
import scala.language.postfixOps
import play.api.libs.json._
import utils.{Logging, Marker}
import play.api.mvc.Call
import software.amazon.awssdk.regions.Region
import scala.concurrent.duration._
trait IndexedItem {
def arn: String
def callFromArn: String => Call
def call: Call = callFromArn(arn)
def fieldIndex: Map[String, String] = Map("arn" -> arn)
}
trait IndexedItemWithCoreTags
extends IndexedItemWithApp
with IndexedItemWithGuCdkVersion
with IndexedItemWithStage
with IndexedItemWithStack
with IndexedItemWithPatternName {
val awsRuntime: String
}
trait IndexedItemWithApp extends IndexedItem {
val app: List[String] = Nil
}
trait IndexedItemWithGuCdkVersion extends IndexedItem {
val guCdkVersion: Option[String] = None
}
trait IndexedItemWithPatternName extends IndexedItem {
val guCdkPatternName: Option[String] = None
}
trait IndexedItemWithStage extends IndexedItem {
val stage: Option[String] = None
}
trait IndexedItemWithStack extends IndexedItem {
val stack: Option[String] = None
}
case class AWSAccount(accountNumber: Option[String], accountName: String)
sealed trait AwsRegionType
case object Global extends AwsRegionType
case object Regional extends AwsRegionType
/** A CollectorSet knows how to create a set of collectors for a given resource
* type that typically spans multiple accounts, which can be of different
* underlying platforms. A CollectorSet creates an appropriate set of Collector
* instances for each account and region.
*
* @param resource
* the name of the resource that this CollectorSet is responsible for
* @param accounts
* the set of accounts to collect this resource from
* @param awsRegionType
* some resourceTypes in AWS have a single Global instance instead of
* Regional instances. If a CollectorSet processes `AmazonOrigin` origins
* then you should specify whether the AWS collector is global (such as
* Route53) or regional (such as EC2 instances).
* @tparam T
* the class that represents a collected instance of the resource
*/
abstract class CollectorSet[T](
val resource: ResourceType,
accounts: Accounts,
val awsRegionType: Option[AwsRegionType]
) extends Logging {
/** Create a collector for the given origin (this is a partial function
* because not all collectors support all types of origin
*/
def lookupCollector: PartialFunction[Origin, Collector[T]]
/** Returns true if the AwsRegionType (Global or Regional) matches the
* origin's region. This means that we can filter on this value when we come
* to create the list of collectors and ensure that Global services crawl
* AWS_GLOBAL only.
*/
def isOriginRegionType(
regionType: Option[AwsRegionType]
)(origin: Origin): Boolean = {
(origin, regionType) match {
case (AmazonOrigin(_, region, _, _, _, _, _, _), Some(Global))
if region != Region.AWS_GLOBAL.id =>
false
case (AmazonOrigin(_, region, _, _, _, _, _, _), Some(Regional))
if region == Region.AWS_GLOBAL.id =>
false
case _ => true
}
}
lazy val collectors: Seq[Collector[T]] =
accounts
.forResource(resource.name)
.filter(isOriginRegionType(awsRegionType))
.flatMap(lookupCollector.lift)
}
trait Collector[T] {
def crawl: Iterable[T]
def origin: Origin
def resource: ResourceType
def crawlRate: CrawlRate
}
object Datum {
def apply[T](collector: Collector[T]): Datum[T] = {
Try {
val items = collector.crawl.toSeq
Datum(Label(collector, items.size), items)
} recover { case NonFatal(t) =>
Datum[T](Label(collector, t), Nil)
} get
}
def empty[T](collector: Collector[T]): Datum[T] = Datum(
Label(collector, new IllegalStateException("First crawl not yet done")),
Nil
)
}
case class Datum[T](label: Label, data: Seq[T])
object Label {
def apply[T](c: Collector[T], itemCount: Int): Label =
Label(c.resource, c.origin, itemCount)
def apply[T](c: Collector[T], error: Throwable): Label =
Label(c.resource, c.origin, 0, error = Some(error))
}
case class Label(
resourceType: ResourceType,
origin: Origin,
itemCount: Int,
createdAt: DateTime = new DateTime(),
error: Option[Throwable] = None
) extends Marker {
lazy val isError: Boolean = error.isDefined
lazy val status: String = if (isError) "error" else "success"
lazy val bestBefore: BestBefore = BestBefore(
createdAt,
origin.crawlRate(resourceType.name).shelfLife,
error = isError
)
override def toMarkerMap: Map[String, Any] =
Map("resource" -> resourceType.name, "account" -> origin.account)
}
case class ResourceType(name: String)
case class CrawlRate(shelfLife: Duration, refreshPeriod: Duration)
case class BestBefore(created: DateTime, shelfLife: Duration, error: Boolean) {
val isFiniteShelfLife: Boolean = shelfLife.isFinite
val bestBefore: DateTime = if (isFiniteShelfLife) {
created plus JodaDuration.millis(shelfLife.toMillis)
} else {
new DateTime(9999, 1, 1, 0, 0, 0, DateTimeZone.UTC)
}
def isStale: Boolean = error || (new DateTime() compareTo bestBefore) >= 0
def age: JodaDuration = new JodaDuration(created, new DateTime)
}
trait JsonCollector[T] extends JsonCollectorTranslator[T, T] with Logging {
def translate(input: T): T = input
}
trait JsonCollectorTranslator[F, T] extends Collector[T] with Logging {
def origin: JsonOrigin
def json: JsValue = origin.data(resource)
def crawlJson(implicit writes: Reads[F]): Iterable[T] = {
Json.fromJson[Seq[F]](json) match {
case JsError(errors) =>
val failure = s"Encountered failure to parse json source: $errors"
log.error(failure)
throw new IllegalArgumentException(failure)
case JsSuccess(result, _) => result.map(translate)
}
}
def translate(input: F): T
}