app/collectors/instance.scala (267 lines of code) (raw):

package collectors import java.net.InetAddress import java.time.Instant import agent._ import conf.AWS import controllers.{Prism, routes} import play.api.mvc.Call import software.amazon.awssdk.services.ec2.Ec2Client import software.amazon.awssdk.services.ec2.model.{ DescribeInstancesRequest, Instance => AwsInstance, Reservation => AwsReservation } import utils.Logging import scala.jdk.CollectionConverters._ import scala.language.postfixOps import scala.util.matching.Regex class InstanceCollectorSet(accounts: Accounts, prism: Prism) extends CollectorSet[Instance]( ResourceType("instance"), accounts, Some(Regional) ) { val lookupCollector: PartialFunction[Origin, Collector[Instance]] = { case amazon: AmazonOrigin => AWSInstanceCollector( amazon, resource, amazon.crawlRate(resource.name), prism ) } } case class AWSInstanceCollector( origin: AmazonOrigin, resource: ResourceType, crawlRate: CrawlRate, prism: Prism ) extends Collector[Instance] with Logging { val client: Ec2Client = Ec2Client.builder .credentialsProvider(origin.credentials.provider) .region(origin.awsRegionV2) .overrideConfiguration(AWS.clientConfig) .build def getInstances: Iterable[(AwsReservation, AwsInstance)] = { val reservations = client .describeInstancesPaginator(DescribeInstancesRequest.builder.build) .reservations .asScala for { reservation <- reservations instance <- reservation.instances.asScala } yield (reservation, instance) } def crawl: Iterable[Instance] = { getInstances .map { case (reservation, instance) => Instance.fromApiData( arn = s"arn:aws:ec2:${origin.region}:${origin.accountNumber .getOrElse(reservation.ownerId)}:instance/${instance.instanceId}", vendorState = Some(instance.state.nameAsString), group = instance.placement.availabilityZone, addresses = AddressList( "public" -> Address( instance.publicDnsName, instance.publicIpAddress ), "private" -> Address( instance.privateDnsName, instance.privateIpAddress ) ), createdAt = instance.launchTime, instanceName = instance.instanceId, region = origin.region, vendor = "aws", securityGroups = instance.securityGroups.asScala.map { sg => Reference[SecurityGroup]( s"arn:aws:ec2:${origin.region}:${origin.accountNumber.get}:security-group/${sg.groupId}", Map( "groupId" -> sg.groupId, "groupName" -> sg.groupName ), prism ) }.toSeq, tags = instance.tags.asScala.map(t => t.key -> t.value).toMap, specs = InstanceSpecification( instance.imageId, Image.arn(origin.region, instance.imageId), instance.instanceTypeAsString, Option(instance.vpcId) ) ) } .map(origin.transformInstance) } } object Instance { def fromApiData( arn: String, vendorState: Option[String], group: String, addresses: AddressList, createdAt: Instant, instanceName: String, region: String, vendor: String, securityGroups: Seq[Reference[SecurityGroup]], tags: Map[String, String], specs: InstanceSpecification ): Instance = { val stack = tags.get("Stack") val app = tags.get("App").map(_.split(",").toList).getOrElse(Nil) apply( arn = arn, name = addresses.primary.dnsName, vendorState = vendorState, group = group, dnsName = addresses.primary.dnsName, ip = addresses.primary.ip, addresses = addresses.mapOfAddresses, createdAt = createdAt, instanceName = instanceName, region = region, vendor = vendor, securityGroups = securityGroups, tags = tags, stage = tags.get("Stage"), stack = stack, app = app, guCdkVersion = tags.get("gu:cdk:version"), guCdkPatternName = tags.get("gu:cdk:pattern-name"), mainclasses = tags .get("Mainclass") .map(_.split(",").toList) .orElse(stack.map(stack => app.map(a => s"$stack::$a"))) .getOrElse(Nil), role = tags.get("Role"), management = ManagementEndpoint .fromTag(addresses.primary.dnsName, tags.get("Management")), specification = Some(specs) ) } } case class ManagementEndpoint( protocol: String, port: Int, path: String, url: String, format: String, source: String ) object ManagementEndpoint { val KeyValue: Regex = """([^=]*)=(.*)""".r def fromTag( dnsName: String, tag: Option[String] ): Option[Seq[ManagementEndpoint]] = { tag match { case Some("none") => None case Some(tagContent) => Some( tagContent .split(";") .filterNot(_.isEmpty) .toIndexedSeq .map { endpoint => val params = endpoint .split(",") .filterNot(_.isEmpty) .flatMap { case KeyValue(key, value) => Some(key -> value) case _ => None } .toMap fromMap(dnsName, params) } ) case None => Some(Seq(fromMap(dnsName))) } } def fromMap( dnsName: String, map: Map[String, String] = Map.empty ): ManagementEndpoint = { val protocol = map.getOrElse("protocol", "http") val port = map.get("port").map(_.toInt).getOrElse(18080) val path = map.getOrElse("path", "/management") val url = s"$protocol://$dnsName:$port$path" val source: String = if (map.isEmpty) "convention" else "tag" ManagementEndpoint( protocol, port, path, url, map.getOrElse("format", "gu"), source ) } } case class AddressList(primary: Address, mapOfAddresses: Map[String, Address]) object AddressList { def apply(addresses: (String, Address)*): AddressList = { val filteredAddresses = addresses.filterNot { case (_, address) => address.dnsName == null || address.ip == null || address.dnsName.isEmpty || address.ip.isEmpty } AddressList( filteredAddresses.headOption.map(_._2).getOrElse(Address.empty), filteredAddresses.toMap ) } } case class Address(dnsName: String, ip: String) object Address { def fromIp(ip: String): Address = Address(InetAddress.getByName(ip).getCanonicalHostName, ip) def fromFQDN(dnsName: String): Address = Address(dnsName, InetAddress.getByName(dnsName).getHostAddress) val empty: Address = Address(null, null) } case class InstanceSpecification( imageId: String, imageArn: String, instanceType: String, vpcId: Option[String] = None ) case class Instance( arn: String, name: String, vendorState: Option[String], group: String, dnsName: String, ip: String, addresses: Map[String, Address], createdAt: Instant, instanceName: String, region: String, vendor: String, securityGroups: Seq[Reference[SecurityGroup]], tags: Map[String, String] = Map.empty, override val stage: Option[String], override val stack: Option[String], override val app: List[String], override val guCdkVersion: Option[String], override val guCdkPatternName: Option[String], mainclasses: List[String], role: Option[String], management: Option[Seq[ManagementEndpoint]], specification: Option[InstanceSpecification] ) extends IndexedItemWithCoreTags { override val awsRuntime: String = "EC2" def callFromArn: String => Call = arn => routes.Api.instance(arn) override lazy val fieldIndex: Map[String, String] = super.fieldIndex ++ Map("dnsName" -> dnsName) ++ stage.map("stage" ->) def +(other: Instance): Instance = { this.copy( mainclasses = (this.mainclasses ++ other.mainclasses).distinct, app = (this.app ++ other.app).distinct, tags = this.tags ++ other.tags ) } def prefixStage(prefix: String): Instance = { this.copy(stage = stage.map(s => s"$prefix$s")) } }