diff --git a/core/src/main/scala/org/broadinstitute/workbench/ccm/CostCalculator.scala b/core/src/main/scala/org/broadinstitute/workbench/ccm/CostCalculator.scala new file mode 100644 index 0000000..245db5a --- /dev/null +++ b/core/src/main/scala/org/broadinstitute/workbench/ccm/CostCalculator.scala @@ -0,0 +1,99 @@ +package org.broadinstitute.workbench.ccm + +import java.time.{Duration, Instant} + +import cats.data.NonEmptyList +import cats.effect.Sync +import cats.effect._ +import cats.implicits._ +import cats.data._ +import org.broadinstitute.workbench.ccm.pricing.{ PriceList} + + +object CostCalculator { + + def getPriceOfCall(callMetaDataJson: MetadataResponse, priceList: PriceList): Either[Throwable, Double] = { + val ls: List[Either[NonEmptyList[String], Double]] = callMetaDataJson.calls.map { call => + getPriceOfCall(call, priceList, Instant.now(), Instant.now()).leftMap(NonEmptyList.one) + } + + ls.parSequence.leftMap(errors => new Exception(errors.toList.mkString(", "))).map(_.sum) + } + + private def getPriceOfCall(call: Call, priceList: PriceList, startTime: Instant, endTime: Instant): Either[String, Double] = { + for { + _ <- if (call.status.asString == "Success") Right(()) else Left(s"Call {name} status was ${call.status.asString}.") // not evaluating workflows that are in flight or Failed or Aborted or whatever + machineType <- if (call.machineType.asString.contains("custom")) Right("custom") else { + Either.catchNonFatal(call.machineType.asString.split("/", 1)).leftMap(_ => "MachineType could not be parsed.")} + } yield { + // ToDo: calculate subworkflows + val isVMPreemptible = preemptible(call) + val wasPreempted = wasCallPreempted(call) + // only looking at actual and not requested disk info + val diskName = call.runtimeAttributes.disks.diskName + val diskSize = call.runtimeAttributes.disks.diskSize.asInt + call.runtimeAttributes.bootDiskSizeGb.asInt + val diskType = call.runtimeAttributes.disks.diskType + val callDurationInSeconds = getCallDuration(call, startTime, endTime) + + // ToDo: add calculating prices for non-custom + // adjust the call duration to account for preemptibility + // if a VM preempted less than 10 minutes after it is created, user incurs no cost + val adjustedCallDurationInSeconds = if (isVMPreemptible && wasPreempted && callDurationInSeconds < (10 * 60)) 0 else callDurationInSeconds + val cpuCost = adjustedCallDurationInSeconds * (if (isVMPreemptible) priceList.CPUPreemptiblePrice else priceList.CPUOnDemandPrice) + val diskCostPerGbHour = if (call.runtimeAttributes.disks.diskType.asString.equals("SSD")) priceList.ssdCostPerGbPerHour else priceList.hddCostPerGbPerHour + val diskGbHours = call.runtimeAttributes.disks.diskSize.asInt * (adjustedCallDurationInSeconds) + val diskCost = diskGbHours * diskCostPerGbHour + val memCost = adjustedCallDurationInSeconds * (if (isVMPreemptible) priceList.RAMPreemptiblePrice else priceList.RAMOnDemandPrice) + cpuCost + diskCost + memCost + } + } + + + private def wasCallPreempted(call: Call): Boolean = { + // treat preempted and retryableFailure as the same + call.executionEvents.exists(event => (event.description.asString.equals("Preempted") || event.description.asString.equals("RetryableFailure"))) + } + + private def preemptible(call: Call): Boolean = { + call.attempt.asInt <= call.runtimeAttributes.preemptible.asInt + // ToDo: Add false result if the metadata does not contain an "attempt" or preemptible info + } + + private def getCallDuration(call: Call, cromwellStartTime: Instant, cromwellEndTime: Instant): Long = { + // ToDo: add option to ignore preempted calls and just return 0 + val papiV2 = call.backend.asString.equals("PAPIv2") + + def getCromwellStart = { + call.executionEvents.find(event => event.description.asString.equals("start")) match { + case Some(nonPapiV2Event) => nonPapiV2Event.startTime + case None => cromwellStartTime + } + } + + def getCromwellEnd = { + call.executionEvents.find(event => event.description.asString.equals("ok")) match { + case Some(nonPapiV2Event) => nonPapiV2Event.endTime + case None => cromwellEndTime + } + } + + val startTime = if (papiV2) { + val startOption = call.executionEvents.find(event => event.description.asString.contains("Preparing Job")) + startOption match { + case Some(event) => event.startTime + case None => getCromwellStart + } + } else getCromwellStart + + val endTime = if (papiV2) { + val endOption = call.executionEvents.find(event => event.description.asString.contains("Worker Released")) + endOption match { + case Some(event) => event.endTime + case None => getCromwellEnd + } + } else getCromwellEnd + + val elapsed = Duration.between(startTime, endTime).getSeconds + if (elapsed >= 60) elapsed else 60 + } +} diff --git a/core/src/main/scala/org/broadinstitute/workbench/ccm/JsonCodec.scala b/core/src/main/scala/org/broadinstitute/workbench/ccm/JsonCodec.scala index c34c093..1c0ae6d 100644 --- a/core/src/main/scala/org/broadinstitute/workbench/ccm/JsonCodec.scala +++ b/core/src/main/scala/org/broadinstitute/workbench/ccm/JsonCodec.scala @@ -1,9 +1,19 @@ package org.broadinstitute.workbench.ccm + +import java.time.format.DateTimeFormatter +import java.time.temporal.{ChronoField, Temporal, TemporalAccessor} +import java.text.SimpleDateFormat +import java.time.Instant + import cats.implicits._ import io.circe.Decoder +import scala.concurrent.duration.{Duration, FiniteDuration} + object JsonCodec { + + val formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") implicit val cpuNumberDecoder: Decoder[CpuNumber] = Decoder.decodeString.emap(s => Either.catchNonFatal(s.toInt).leftMap(_.getMessage).map(CpuNumber)) implicit val bootDiskSizeGbDecoder: Decoder[BootDiskSizeGb] = Decoder.decodeString.emap(x => Either.catchNonFatal(x.toInt).leftMap(_.getMessage).map(BootDiskSizeGb)) implicit val preemptibleDecoder: Decoder[Preemptible] = Decoder.decodeString.emap{ @@ -18,17 +28,43 @@ object JsonCodec { size <- Either.catchNonFatal(array(1).toInt).leftMap(_.getMessage) } yield Disks(DiskName(array(0)), DiskSize(size), DiskType(array(2))) } + + implicit val runtimeAttributesDecoder: Decoder[RuntimeAttributes] = Decoder.forProduct4("cpu", "disks", "bootDiskSizeGb", "preemptible")(RuntimeAttributes) - implicit val callDecoder: Decoder[Call] = Decoder.instance{ + + implicit val executionEventDecoder: Decoder[ExecutionEvent] = Decoder.instance { cursor => + + for { + description <- cursor.downField("description").as[String] + startTime <- cursor.downField("startTime").as[Instant] + endTime <- cursor.downField("endTime").as[Instant] + } yield ExecutionEvent(ExecutionEventDescription(description), startTime, endTime) + + } + + implicit val callDecoder: Decoder[Call] = Decoder.instance { cursor => for { ra <- cursor.downField("runtimeAttributes").as[RuntimeAttributes] + executionEvents <- cursor.downField("executionEvents").as[List[ExecutionEvent]] isPreemptible <- cursor.downField("preemptible").as[Boolean] isCallCaching <- cursor.downField("callCaching").downField("hit").as[Boolean] - } yield Call(ra, isCallCaching, isPreemptible) + region <- cursor.downField("jes").downField("zone").as[String] + machineType <- cursor.downField("jes").downField("machineType").as[String] + status <- cursor.downField("backendStatus").as[String] + backend <- cursor.downField("backend").as[String] + attempt <- cursor.downField("attempt").as[Int] + } yield Call(ra, executionEvents, isCallCaching, isPreemptible, Region(region), Status(status), MachineType(machineType), BackEnd(backend), Attempt(attempt)) } + implicit val metadataResponseDecoder: Decoder[MetadataResponse] = Decoder.instance { cursor => - cursor.downField("calls").as[Map[String, List[Call]]].map(x => MetadataResponse(x.values.flatten.toList)) + for { + calls <- cursor.downField("calls").as[Map[String, List[Call]]].map(x => (x.values.flatten.toList)) + start <- cursor.downField("start").as[Instant] + end <- cursor.downField("end").as[Instant] + } yield MetadataResponse(calls, start, end) } + + } \ No newline at end of file diff --git a/core/src/main/scala/org/broadinstitute/workbench/ccm/model.scala b/core/src/main/scala/org/broadinstitute/workbench/ccm/model.scala index 5555bae..0add09b 100644 --- a/core/src/main/scala/org/broadinstitute/workbench/ccm/model.scala +++ b/core/src/main/scala/org/broadinstitute/workbench/ccm/model.scala @@ -1,5 +1,8 @@ package org.broadinstitute.workbench.ccm +import java.time.Instant + + final case class Cpu(asString: String) extends AnyVal final case class CpuNumber(asInt: Int) extends AnyVal final case class BootDiskSizeGb(asInt: Int) extends AnyVal @@ -9,9 +12,36 @@ final case class DiskName(asString: String) extends AnyVal final case class DiskSize(asInt: Int) extends AnyVal final case class DiskType(asString: String) extends AnyVal final case class Preemptible(asInt: Int) extends AnyVal -final case class MetadataResponse(value: List[Call]) extends AnyVal +final case class Attempt(asInt: Int) extends AnyVal +final case class Region(asString: String) extends AnyVal +final case class Status(asString: String) extends AnyVal +final case class MachineType(asString: String) extends AnyVal +final case class ExecutionEventDescription(asString: String) extends AnyVal +final case class BackEnd(asString: String) extends AnyVal + + + +final case class ExecutionEvent(description: ExecutionEventDescription, + startTime: Instant, + endTime: Instant) + +final case class Call(runtimeAttributes: RuntimeAttributes, + executionEvents: List[ExecutionEvent], + isCallCaching: Boolean, + preemptible: Boolean, + region: Region, + status: Status, + machineType: MachineType, + backend: BackEnd, + attempt: Attempt) + +final case class MetadataResponse(calls: List[Call], startTime: Instant, endTime: Instant) + + +final case class RuntimeAttributes(cpuNumber: CpuNumber, + disks: Disks, + bootDiskSizeGb: BootDiskSizeGb, + preemptible: Preemptible) -final case class Call(runtimeAttributes: RuntimeAttributes, isCallCaching: Boolean, preemptible: Boolean) -final case class RuntimeAttributes(cpuNumber: CpuNumber, disks: Disks, bootDiskSizeGb: BootDiskSizeGb, preemptible: Preemptible) final case class Disks(diskName: DiskName, diskSize: DiskSize, diskType: DiskType) final case class Compute(cpu: Cpu, ram: Ram) diff --git a/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/GcpPricing.scala b/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/GcpPricing.scala index e076211..0dbf58c 100644 --- a/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/GcpPricing.scala +++ b/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/GcpPricing.scala @@ -4,17 +4,38 @@ package pricing import cats.effect.Sync import io.circe.Json import org.broadinstitute.workbench.ccm.pricing.JsonCodec._ -import org.http4s.Uri +import org.http4s.{EntityDecoder, Uri} import org.http4s.circe.CirceEntityDecoder._ import org.http4s.client.Client + +final case class PriceList(region: Region, + machineType: MachineType, + ssdCostPerGbPerHour: Double, + hddCostPerGbPerHour: Double, + CPUOnDemandPrice: Double, + RAMOnDemandPrice: Double, + extendedOnDemandRAMPrice: Double, + CPUPreemptiblePrice: Double, + RAMPreemptiblePrice: Double, + extendedRAMPreemptiblePrice: Double) + +case class GcpPriceList(asJson: Json) extends AnyVal + class GcpPricing[F[_]: Sync](httpClient: Client[F], uri: Uri) { - def getPriceList(): F[GcpPriceList] = { - httpClient.expect[GcpPriceList](uri) + + def getGcpPriceList(region: Region, machineType: MachineType): F[PriceList] = { + // F can not be flatmapped +// for { +// json <- httpClient.expect[Json](uri) +// result <- JsonCodec.PriceListDecoder(region, machineType).decodeJson(json) +// } yield result + + implicit val priceListDecoder = JsonCodec.PriceListDecoder(region, machineType) + httpClient.expect[PriceList](uri) } } -final case class GcpPriceList(asJson: Json) extends AnyVal //CUSTOM_MACHINE_CPU = "CP-DB-PG-CUSTOM-VM-CORE" //CUSTOM_MACHINE_RAM = "CP-DB-PG-CUSTOM-VM-RAM" diff --git a/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/JsonCodec.scala b/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/JsonCodec.scala index 90955c9..655ba16 100644 --- a/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/JsonCodec.scala +++ b/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/JsonCodec.scala @@ -1,7 +1,7 @@ package org.broadinstitute.workbench.ccm package pricing -import io.circe.Decoder +import io.circe.{Decoder, DecodingFailure} //TODO: this needs to be updated to use https://cloud.google.com/billing/v1/how-tos/catalog-api object JsonCodec { @@ -9,4 +9,112 @@ object JsonCodec { implicit val ramDecoder: Decoder[RamCost] = Decoder.forProduct1("us")(RamCost.apply) implicit val computeCostDecoder: Decoder[ComputeCost] = Decoder.forProduct2("CP-DB-PG-CUSTOM-VM-CORE", "CP-DB-PG-CUSTOM-VM-RAM")(ComputeCost.apply) implicit val gcpPriceListDecoder: Decoder[GcpPriceList] = Decoder.forProduct1("gcp_price_list")(GcpPriceList.apply) + + + // implicit val categoryDecoder: Decoder[Category] = Decoder.forProduct4("serviceDisplayName", "resourceFamily", "resourceGroup", "usageType")(Category.apply) + implicit val categoryDecoder: Decoder[Category] = Decoder.instance { cursor => + for { + serviceDisplayName <- cursor.downField("serviceDisplayName").as[String] + resourceFamily <- cursor.downField("resourceFamily").as[String] + resourceGroup <- cursor.downField("resourceGroup").as[String] + usageType <- cursor.downField("usageType").as[String] + } yield Category(ServiceDisplayName(serviceDisplayName), ResourceFamily(resourceFamily), ResourceGroup(resourceGroup), UsageType(usageType)) + } + + implicit val tieredRateDecoder: Decoder[TieredRate] = Decoder.instance { cursor => + for { + startUsageAmount <- cursor.downField("startUsageAmount").as[Int] + currencyCode <- cursor.downField("unitPrice").downField("currencyCode").as[String] + units <- cursor.downField("unitPrice").downField("units").as[Int] + nanos <- cursor.downField("unitPrice").downField("nanos").as[Int] + } yield TieredRate(StartUsageAmount(startUsageAmount), CurrencyCode(currencyCode), Units(units), Nanos(nanos)) + } + + implicit val pricingInfoDecoder: Decoder[PricingInfo] = Decoder.instance { cursor => + for { + usageUnit <- cursor.downField("pricingExpression").downField("usageUnit").as[String] + tieredRates <- cursor.downField("pricingExpression").downField("tieredRates").as[List[TieredRate]] + } yield PricingInfo(UsageUnit(usageUnit), tieredRates) + } + + implicit val googlePriceItemDecoder: Decoder[GooglePriceItem] = Decoder.instance { + // put filtering in here! + cursor => + for { + name <- cursor.downField("name").as[String] + skuId <- cursor.downField("skuId").as[String] + description <- cursor.downField("description").as[String] + category <- cursor.downField("category").as[Category] + regions <- cursor.downField("serviceRegions").as[List[String]] + pricingInfo <- cursor.downField("pricingInfo").as[List[PricingInfo]] + } yield GooglePriceItem(SkuName(name), SkuId(skuId), SkuDescription(description), category, regions.map(Region(_)), pricingInfo) + } + + def PriceListDecoder(region: Region, machineType: MachineType): Decoder[PriceList] = Decoder.instance { + cursor => + def getPrice(googlePriceItems: List[GooglePriceItem], resourceFamily: ResourceFamily, resourceGroup: ResourceGroup, usageType: UsageType, descriptionShouldInclude: Option[String], descriptionShouldNotInclude: Option[String]): Either[DecodingFailure, Double] = { + val sku = googlePriceItems.filter { priceItem => + (priceItem.regions.contains(region) + && priceItem.category.resourceFamily.equals(resourceFamily) + && priceItem.category.resourceGroup.equals(resourceGroup) + && priceItem.category.usageType.equals(usageType) + && (descriptionShouldInclude match { + case Some(desc) => priceItem.description.asString.contains(desc) + case None => true}) + && (descriptionShouldNotInclude match { + case Some(desc) => !priceItem.description.asString.contains(desc) + case None => true})) + } + sku.length match { + case 0 => Left(DecodingFailure(s"No SKUs matched with region $region, resourceFamily $resourceFamily, resourceGroup $resourceGroup, $usageType usageType, and description including $descriptionShouldInclude and notIncluding $descriptionShouldNotInclude in the following price list: $googlePriceItems", List())) + case 1 => Right(getPriceFromSku(sku.head)) + case tooMany => Left(DecodingFailure(s"$tooMany SKUs matched with region $region, resourceFamily $resourceFamily, resourceGroup $resourceGroup, $usageType usageType, and description including $descriptionShouldInclude and notIncluding $descriptionShouldNotInclude in the following price list: $googlePriceItems", List())) + } + } + + def getPriceFromSku(priceItem: GooglePriceItem): Double = { + // ToDo: Currently just takes first, make it take either most recent or make it dependent on when the call ran + priceItem.pricingInfo.head.tieredRates.filter(rate => rate.startUsageAmount.asInt == 0).head.nanos.asInt.toDouble / 1000000000 + } + + def priceList(googlePriceList: GooglePriceList): Either[DecodingFailure, PriceList] = { + println(s"GOOGLE PRICE LIST: $googlePriceList") + val filteredByRegion = googlePriceList.priceItems.filter(priceItem => priceItem.regions.contains(region)) + println(s"FILTERED BY REGION: $filteredByRegion" ) + for { + ssdCostPerGbPerMonth <- getPrice(filteredByRegion, ResourceFamily("Storage"), ResourceGroup("SSD"), UsageType("OnDemand"), None, Some("Regional")) + hddCostPerGbPerMonth <- getPrice(filteredByRegion, ResourceFamily("Storage"), ResourceGroup("PDStandard"), UsageType("OnDemand"), None, Some("Regional")) + cpuOnDemandCostGibibytesPerHour <- getPrice(filteredByRegion, ResourceFamily("Compute"), ResourceGroup("CPU"), UsageType("OnDemand"), None, None) + ramOnDemandCostGibibytesPerHour <- getPrice(filteredByRegion, ResourceFamily("Compute"), ResourceGroup("RAM"), UsageType("OnDemand"), None, Some("Custom Extended")) + extendedRamOnDemandCostGibibytesPerHour <- getPrice(filteredByRegion, ResourceFamily("Compute"), ResourceGroup("RAM"), UsageType("OnDemand"), Some("Custom Extended"), None) + cpuPreemptibleCostGibibytesPerHour <- getPrice(filteredByRegion, ResourceFamily("Compute"), ResourceGroup("CPU"), UsageType("Preemptible"), None, Some("Custom Extended")) + ramPreemptibleCostGibibytesPerHour <- getPrice(filteredByRegion, ResourceFamily("Compute"), ResourceGroup("RAM"), UsageType("Preemptible"), None, Some("Custom Extended")) + extendedRamPreemptibleCostGibibytesPerHour <- getPrice(filteredByRegion, ResourceFamily("Compute"), ResourceGroup("RAM"), UsageType("Preemptible"), Some("Custom Extended"), None) + } yield { + val ssdCostPerGbPerHour = ssdCostPerGbPerMonth / (24 * 365 / 12) + val hddCostPerGbPerHour = hddCostPerGbPerMonth / (24 * 365 / 12) + PriceList( + region, + machineType, + ssdCostPerGbPerHour, + hddCostPerGbPerHour, + cpuOnDemandCostGibibytesPerHour, + ramOnDemandCostGibibytesPerHour, + extendedRamOnDemandCostGibibytesPerHour, + cpuPreemptibleCostGibibytesPerHour, + ramPreemptibleCostGibibytesPerHour, + extendedRamPreemptibleCostGibibytesPerHour + ) + } + } + + for { + googlePriceList <- googlePriceListDecoder.apply(cursor) + result <- priceList(googlePriceList) + } yield { + result + } + } + + implicit val googlePriceListDecoder: Decoder[GooglePriceList] = Decoder.forProduct1("skus")(GooglePriceList.apply) } \ No newline at end of file diff --git a/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/model.scala b/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/model.scala index 15337f7..751b25b 100644 --- a/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/model.scala +++ b/core/src/main/scala/org/broadinstitute/workbench/ccm/pricing/model.scala @@ -1,6 +1,7 @@ package org.broadinstitute.workbench.ccm.pricing import io.circe.Decoder +import org.broadinstitute.workbench.ccm.Region final case class CpuCost(asDouble: Double) extends AnyVal final case class RamCost(asDouble: Double) extends AnyVal @@ -8,3 +9,25 @@ final case class RamCost(asDouble: Double) extends AnyVal final case class ComputeCost(cpuCost: CpuCost, ramCost: RamCost) { val totalCost = cpuCost.asDouble + ramCost.asDouble //TODO: update this } + +final case class SkuName(asString: String) extends AnyVal +final case class SkuId(asString: String) extends AnyVal +final case class SkuDescription(asString: String) extends AnyVal +final case class ServiceDisplayName(asString: String) extends AnyVal +final case class ResourceFamily(asString: String) extends AnyVal +final case class ResourceGroup(asString: String) extends AnyVal +final case class UsageType(asString: String) extends AnyVal +final case class UsageUnit(asString: String) extends AnyVal +final case class StartUsageAmount(asInt: Int) extends AnyVal +final case class CurrencyCode(asString: String) extends AnyVal +final case class Units(asInt: Int) extends AnyVal +final case class Nanos(asInt: Int) extends AnyVal + + +final case class TieredRate(startUsageAmount: StartUsageAmount, currencyCode: CurrencyCode, units: Units, nanos: Nanos) +final case class PricingInfo(usageUnit: UsageUnit, tieredRates: List[TieredRate]) +final case class Category(serviceDisplayName: ServiceDisplayName, resourceFamily: ResourceFamily, resourceGroup: ResourceGroup, usageType: UsageType) +final case class GooglePriceItem(name: SkuName, skuId: SkuId, description: SkuDescription, category: Category, regions: List[Region], pricingInfo: List[PricingInfo]) +final case class GooglePriceList(priceItems: List[GooglePriceItem]) + + diff --git a/core/src/test/scala/org/broadinstitute/workbench/ccm/JsonCodecTest.scala b/core/src/test/scala/org/broadinstitute/workbench/ccm/JsonCodecTest.scala index db141c2..b032587 100644 --- a/core/src/test/scala/org/broadinstitute/workbench/ccm/JsonCodecTest.scala +++ b/core/src/test/scala/org/broadinstitute/workbench/ccm/JsonCodecTest.scala @@ -1,11 +1,19 @@ package org.broadinstitute.workbench.ccm +import java.text.SimpleDateFormat +import java.time.Instant + import minitest.SimpleTestSuite import io.circe.parser._ import JsonCodec._ +import scala.concurrent.duration.Duration + object JsonCodecTest extends CcmTestSuite { + test("metadataResponseDecoder should be able to decode MetadataResponse"){ + val formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") + def toInstant(time: String): Instant = formatter.parse(time).toInstant val res = for { json <- parse(sampleTest) r <- json.as[MetadataResponse] @@ -13,9 +21,32 @@ object JsonCodecTest extends CcmTestSuite { val expectedResponse = MetadataResponse( List(Call( RuntimeAttributes(CpuNumber(1), Disks(DiskName("local-disk"), DiskSize(1), DiskType("HDD")), BootDiskSizeGb(10), Preemptible(3)), + List(ExecutionEvent(ExecutionEventDescription("delocalizing-files"), Instant.parse("2019-01-02T22:14:05.438689657Z"), Instant.parse("2019-01-02T22:14:09.779343193Z")), + ExecutionEvent(ExecutionEventDescription("UpdatingJobStore"), Instant.parse("2019-01-02T22:14:39.825Z"), Instant.parse("2019-01-02T22:14:40.799Z")), + ExecutionEvent(ExecutionEventDescription("ok"), Instant.parse("2019-01-02T22:14:09.779343193Z"), Instant.parse("2019-01-02T22:14:10Z")), + ExecutionEvent(ExecutionEventDescription("waiting for quota"), Instant.parse("2019-01-02T22:11:04Z"), Instant.parse("2019-01-02T22:11:27Z")), + ExecutionEvent(ExecutionEventDescription("RequestingExecutionToken"), Instant.parse("2019-01-02T22:10:13.687Z"), Instant.parse("2019-01-02T22:10:13.979Z")), + ExecutionEvent(ExecutionEventDescription("RunningJob"), Instant.parse("2019-01-02T22:11:02.884Z"), Instant.parse("2019-01-02T22:11:04Z")), + ExecutionEvent(ExecutionEventDescription("UpdatingCallCache"), Instant.parse("2019-01-02T22:14:39.160Z"), Instant.parse("2019-01-02T22:14:39.825Z")), + ExecutionEvent(ExecutionEventDescription("pulling-image"), Instant.parse("2019-01-02T22:12:47.780575142Z"), Instant.parse("2019-01-02T22:12:52.779343466Z")), + ExecutionEvent(ExecutionEventDescription("cromwell poll interval"), Instant.parse("2019-01-02T22:14:10Z"), Instant.parse("2019-01-02T22:14:39.160Z")), + ExecutionEvent(ExecutionEventDescription("localizing-files"), Instant.parse("2019-01-02T22:12:52.779343466Z"), Instant.parse("2019-01-02T22:14:04.589980901Z")), + ExecutionEvent(ExecutionEventDescription("Pending"), Instant.parse("2019-01-02T22:10:13.686Z"), Instant.parse("2019-01-02T22:10:13.687Z")), + ExecutionEvent(ExecutionEventDescription("start"), Instant.parse("2019-01-02T22:12:46.103634373Z"), Instant.parse("2019-01-02T22:12:47.780575142Z")), + ExecutionEvent(ExecutionEventDescription("WaitingForValueStore"), Instant.parse("2019-01-02T22:10:13.979Z"), Instant.parse("2019-01-02T22:10:13.979Z")), + ExecutionEvent(ExecutionEventDescription("initializing VM"), Instant.parse("2019-01-02T22:11:27Z"), Instant.parse("2019-01-02T22:12:46.103634373Z")), + ExecutionEvent(ExecutionEventDescription("running-docker"), Instant.parse("2019-01-02T22:14:04.589980901Z"), Instant.parse("2019-01-02T22:14:05.438689657Z")), + ExecutionEvent(ExecutionEventDescription("CheckingCallCache"), Instant.parse("2019-01-02T22:11:02.874Z"), Instant.parse("2019-01-02T22:11:02.884Z")), + ExecutionEvent(ExecutionEventDescription("PreparingJob"), Instant.parse("2019-01-02T22:10:13.979Z"), Instant.parse("2019-01-02T22:11:02.874Z"))), false, - true - )) + true, + Region("us-central1-c"), + Status("Success"), + MachineType("us-central1-c/f1-micro"), + BackEnd("JES"), + Attempt(1))), + Instant.parse("2019-01-02T22:10:07.088Z"), + Instant.parse("2019-01-02T22:14:47.266Z") ) assertEquals(r, expectedResponse) } diff --git a/core/src/test/scala/org/broadinstitute/workbench/ccm/pricing/GcpPriceListTest.scala b/core/src/test/scala/org/broadinstitute/workbench/ccm/pricing/GcpPriceListTest.scala new file mode 100644 index 0000000..4fe6ad6 --- /dev/null +++ b/core/src/test/scala/org/broadinstitute/workbench/ccm/pricing/GcpPriceListTest.scala @@ -0,0 +1,14 @@ +package org.broadinstitute.workbench.ccm.pricing +import java.text.SimpleDateFormat +import java.time.Instant + +import minitest.SimpleTestSuite +import io.circe.parser._ +import JsonCodec._ +import org.broadinstitute.workbench.ccm._ + + +object GcpPricingTest extends CcmTestSuite { + +} + diff --git a/core/src/test/scala/org/broadinstitute/workbench/ccm/pricing/JsonCodecTest.scala b/core/src/test/scala/org/broadinstitute/workbench/ccm/pricing/JsonCodecTest.scala new file mode 100644 index 0000000..0a30805 --- /dev/null +++ b/core/src/test/scala/org/broadinstitute/workbench/ccm/pricing/JsonCodecTest.scala @@ -0,0 +1,616 @@ +package org.broadinstitute.workbench.ccm.pricing + +import minitest.SimpleTestSuite +import io.circe.parser._ +import JsonCodec._ +import org.broadinstitute.workbench.ccm.{CcmTestSuite, MachineType, Region} + +object JsonCodecTest extends CcmTestSuite { + test("SKUsDecoder should be able to decode SKUs"){ + val res = for { + json <- parse(sampleTest) + r <- json.as[GooglePriceList] + } yield { + val expectedResponse = GooglePriceList( + List( + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/472A-2C0D-17F3"), + SkuId("472A-2C0D-17F3"), + SkuDescription("Preemptible Custom Extended Instance Ram running in Los Angeles"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Compute"), + ResourceGroup("RAM"), + UsageType("Preemptible")), + List(Region("us-west2")), + List(PricingInfo(UsageUnit("GiBy.h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(10931000)))))), + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/472A-2C0D-17F3"), + SkuId("472A-2C0D-17F3"), + SkuDescription("Custom Extended Instance Ram running in Los Angeles"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Compute"), + ResourceGroup("RAM"), + UsageType("OnDemand")), + List(Region("us-west2")), + List(PricingInfo(UsageUnit("GiBy.h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(10931000)))))), + + //i think this one might be wrong + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/5662-928E-19C3"), + SkuId("5662-928E-19C3"), + SkuDescription("Preemptible Custom Instance Ram running in Los Angeles"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Compute"), + ResourceGroup("RAM"), + UsageType("Preemptible")), + List(Region("us-west2")), + List(PricingInfo(UsageUnit("GiBy.h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(1076000)))))), + + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/4EAF-BBAF-9069"), + SkuId("4EAF-BBAF-9069"), + SkuDescription("Preemptible Custom Instance Core running in Los Angeles"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Compute"), + ResourceGroup("CPU"), + UsageType("Preemptible")), + List(Region("us-west2")), + List(PricingInfo(UsageUnit("h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(7986000)))))), + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/5662-928E-19C3"), + SkuId("5662-928E-19C3"), + SkuDescription("Custom Instance Ram running in Los Angeles"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Compute"), + ResourceGroup("RAM"), + UsageType("OnDemand")), + List(Region("us-west2")), + List(PricingInfo(UsageUnit("GiBy.h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(1076000)))))), + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/2037-B859-1728"), + SkuId("2037-B859-1728"), + SkuDescription("Custom Instance Core running in Los Angeles"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Compute"), + ResourceGroup("CPU"), + UsageType("OnDemand")), + List(Region("us-west2")), + List(PricingInfo(UsageUnit("h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(37970000)))))), + + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/878F-E2CC-F899"), + SkuId("878F-E2CC-F899"), + SkuDescription("Storage PD Capacity in Los Angeles"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Storage"), + ResourceGroup("PDStandard"), + UsageType("OnDemand")), + List(Region("us-west2")), + List(PricingInfo(UsageUnit("GiBy.mo"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(96000000)))))), + + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/0013-863C-A2FF"), + SkuId("0013-863C-A2FF"), + SkuDescription("Licensing Fee for SQL Server 2016 Standard on VM with 18 VCPU"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("License"), + ResourceGroup("SQLServer2016Standard"), + UsageType("OnDemand")), + List(Region("global")), + List(PricingInfo(UsageUnit("h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(2), Nanos(961000000)))))), + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/001D-204A-23DA"), + SkuId("001D-204A-23DA"), + SkuDescription("Commitment v1: Cpu in Montreal for 1 Year"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Compute"), + ResourceGroup("CPU"), + UsageType("Commit1Yr")), + List(Region("northamerica-northeast1")), + List(PricingInfo(UsageUnit("h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(21925000)))))), + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/0589-AA00-68BD"), + SkuId("0589-AA00-68BD"), + SkuDescription("SSD backed PD Capacity in Los Angeles"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Storage"), + ResourceGroup("SSD"), + UsageType("OnDemand")), + List(Region("us-west2")), + List(PricingInfo(UsageUnit("GiBy.mo"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(204000000)))))), + + GooglePriceItem( + SkuName("services/6F81-5844-456A/skus/077F-E880-3C8E"), + SkuId("077F-E880-3C8E"), + SkuDescription("Preemptible Custom Extended Instance Core running in Sydney"), + Category(ServiceDisplayName("Compute Engine"), + ResourceFamily("Compute"), + ResourceGroup("CPU"), + UsageType("Preemptible")), + List(Region("australia-southeast1")), + List(PricingInfo(UsageUnit("h"), List(TieredRate(StartUsageAmount(0), CurrencyCode("USD"), Units(0), Nanos(8980000)))))))) + + assertEquals(r, expectedResponse) + } + res.fold[Unit](e => throw e, identity) + } + + test("SKUsDecoder should be able to decode PriceList"){ + val region = Region("us-west2") + val machineType = MachineType("custom") + implicit val priceListDecoder = JsonCodec.PriceListDecoder(region, machineType) + val res = for { + json <- parse(sampleTest) + r <- json.as[PriceList] + } yield { + val expectedResponse = PriceList(region, + machineType, + 0.0002794520547945205, + 0.0001315068493150685, + 0.03797, + 0.001076, + 0.010931, + 0.007986, + 0.001076, + 0.010931) + assertEquals(r, expectedResponse) + } + res.fold[Unit](e => throw e, identity) + } + + val sampleTest: String = + """ + |{ + | "skus": [ + | { + | "name": "services/6F81-5844-456A/skus/472A-2C0D-17F3", + | "skuId": "472A-2C0D-17F3", + | "description": "Preemptible Custom Extended Instance Ram running in Los Angeles", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Compute", + | "resourceGroup": "RAM", + | "usageType": "Preemptible" + | }, + | "serviceRegions": [ + | "us-west2" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "GiBy.h", + | "usageUnitDescription": "gibibyte hour", + | "baseUnit": "By.s", + | "baseUnitDescription": "byte second", + | "baseUnitConversionFactor": 3865470566400, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 10931000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-17T13:07:15.915Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/472A-2C0D-17F3", + | "skuId": "472A-2C0D-17F3", + | "description": "Custom Extended Instance Ram running in Los Angeles", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Compute", + | "resourceGroup": "RAM", + | "usageType": "OnDemand" + | }, + | "serviceRegions": [ + | "us-west2" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "GiBy.h", + | "usageUnitDescription": "gibibyte hour", + | "baseUnit": "By.s", + | "baseUnitDescription": "byte second", + | "baseUnitConversionFactor": 3865470566400, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 10931000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-17T13:07:15.915Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/5662-928E-19C3", + | "skuId": "5662-928E-19C3", + | "description": "Preemptible Custom Instance Ram running in Los Angeles", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Compute", + | "resourceGroup": "RAM", + | "usageType": "Preemptible" + | }, + | "serviceRegions": [ + | "us-west2" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "GiBy.h", + | "usageUnitDescription": "gibibyte hour", + | "baseUnit": "By.s", + | "baseUnitDescription": "byte second", + | "baseUnitConversionFactor": 3865470566400, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 1076000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-17T13:07:15.915Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/4EAF-BBAF-9069", + | "skuId": "4EAF-BBAF-9069", + | "description": "Preemptible Custom Instance Core running in Los Angeles", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Compute", + | "resourceGroup": "CPU", + | "usageType": "Preemptible" + | }, + | "serviceRegions": [ + | "us-west2" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "h", + | "usageUnitDescription": "hour", + | "baseUnit": "s", + | "baseUnitDescription": "second", + | "baseUnitConversionFactor": 3600, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 7986000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-17T13:07:15.915Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/5662-928E-19C3", + | "skuId": "5662-928E-19C3", + | "description": "Custom Instance Ram running in Los Angeles", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Compute", + | "resourceGroup": "RAM", + | "usageType": "OnDemand" + | }, + | "serviceRegions": [ + | "us-west2" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "GiBy.h", + | "usageUnitDescription": "gibibyte hour", + | "baseUnit": "By.s", + | "baseUnitDescription": "byte second", + | "baseUnitConversionFactor": 3865470566400, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 1076000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-17T13:07:15.915Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/2037-B859-1728", + | "skuId": "2037-B859-1728", + | "description": "Custom Instance Core running in Los Angeles", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Compute", + | "resourceGroup": "CPU", + | "usageType": "OnDemand" + | }, + | "serviceRegions": [ + | "us-west2" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "h", + | "usageUnitDescription": "hour", + | "baseUnit": "s", + | "baseUnitDescription": "second", + | "baseUnitConversionFactor": 3600, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 37970000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-17T13:07:15.915Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/878F-E2CC-F899", + | "skuId": "878F-E2CC-F899", + | "description": "Storage PD Capacity in Los Angeles", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Storage", + | "resourceGroup": "PDStandard", + | "usageType": "OnDemand" + | }, + | "serviceRegions": [ + | "us-west2" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "GiBy.mo", + | "usageUnitDescription": "gibibyte month", + | "baseUnit": "By.s", + | "baseUnitDescription": "byte second", + | "baseUnitConversionFactor": 2.8759101014016e+15, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 96000000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-17T13:07:15.915Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/0013-863C-A2FF", + | "skuId": "0013-863C-A2FF", + | "description": "Licensing Fee for SQL Server 2016 Standard on VM with 18 VCPU", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "License", + | "resourceGroup": "SQLServer2016Standard", + | "usageType": "OnDemand" + | }, + | "serviceRegions": [ + | "global" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "h", + | "usageUnitDescription": "hour", + | "baseUnit": "s", + | "baseUnitDescription": "second", + | "baseUnitConversionFactor": 3600, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "2", + | "nanos": 961000000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-04T01:08:22.878Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/001D-204A-23DA", + | "skuId": "001D-204A-23DA", + | "description": "Commitment v1: Cpu in Montreal for 1 Year", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Compute", + | "resourceGroup": "CPU", + | "usageType": "Commit1Yr" + | }, + | "serviceRegions": [ + | "northamerica-northeast1" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "h", + | "usageUnitDescription": "hour", + | "baseUnit": "s", + | "baseUnitDescription": "second", + | "baseUnitConversionFactor": 3600, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 21925000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-04T01:08:22.878Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/0589-AA00-68BD", + | "skuId": "0589-AA00-68BD", + | "description": "SSD backed PD Capacity in Los Angeles", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Storage", + | "resourceGroup": "SSD", + | "usageType": "OnDemand" + | }, + | "serviceRegions": [ + | "us-west2" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "GiBy.mo", + | "usageUnitDescription": "gibibyte month", + | "baseUnit": "By.s", + | "baseUnitDescription": "byte second", + | "baseUnitConversionFactor": 2.8759101014016e+15, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 204000000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-04T01:08:22.878Z" + | } + | ], + | "serviceProviderName": "Google" + | }, + | { + | "name": "services/6F81-5844-456A/skus/077F-E880-3C8E", + | "skuId": "077F-E880-3C8E", + | "description": "Preemptible Custom Extended Instance Core running in Sydney", + | "category": { + | "serviceDisplayName": "Compute Engine", + | "resourceFamily": "Compute", + | "resourceGroup": "CPU", + | "usageType": "Preemptible" + | }, + | "serviceRegions": [ + | "australia-southeast1" + | ], + | "pricingInfo": [ + | { + | "summary": "", + | "pricingExpression": { + | "usageUnit": "h", + | "usageUnitDescription": "hour", + | "baseUnit": "s", + | "baseUnitDescription": "second", + | "baseUnitConversionFactor": 3600, + | "displayQuantity": 1, + | "tieredRates": [ + | { + | "startUsageAmount": 0, + | "unitPrice": { + | "currencyCode": "USD", + | "units": "0", + | "nanos": 8980000 + | } + | } + | ] + | }, + | "currencyConversionRate": 1, + | "effectiveTime": "2019-01-04T01:08:22.878Z" + | } + | ], + | "serviceProviderName": "Google" + | } + | ] + | } + """.stripMargin +} diff --git a/server/src/main/scala/org/broadinstitute/workbench/ccm/server/CcmGrpcImp.scala b/server/src/main/scala/org/broadinstitute/workbench/ccm/server/CcmGrpcImp.scala index 0b249b8..dad6f7c 100644 --- a/server/src/main/scala/org/broadinstitute/workbench/ccm/server/CcmGrpcImp.scala +++ b/server/src/main/scala/org/broadinstitute/workbench/ccm/server/CcmGrpcImp.scala @@ -1,21 +1,25 @@ package org.broadinstitute.workbench.ccm package server +import java.time.Instant + import cats.effect.Sync import cats.implicits._ import io.chrisdavenport.log4cats.Logger import io.grpc.Metadata import org.broadinstitute.workbench.ccm.pricing.JsonCodec._ -import org.broadinstitute.workbench.ccm.pricing.{ComputeCost, GcpPricing} +import org.broadinstitute.workbench.ccm.pricing.GcpPricing import org.broadinstitute.workbench.ccm.protos.ccm._ class CcmGrpcImp[F[_]: Sync: Logger](pricing: GcpPricing[F]) extends CcmFs2Grpc[F] { override def getWorkflowCost(request: WorkflowCostRequest, clientHeaders: Metadata): F[WorkflowCostResponse] = { for { - priceList <- pricing.getPriceList() - computeCost <- Sync[F].rethrow(Sync[F].delay[Either[Throwable, ComputeCost]](priceList.asJson.as[ComputeCost].leftWiden)) + // cromwellMetadata: MetadataResponse <- ??? + //priceList <- pricing.getGcpPriceList(cromwellMetadata.calls.head.region, MachineType("custom")) + priceList <- pricing.getGcpPriceList(Region(""), MachineType("")) + result <- Sync[F].rethrow(Sync[F].delay[Either[Throwable, Double]](CostCalculator.getPriceOfCall(sampleMetaData, priceList))) } yield { - WorkflowCostResponse(computeCost.totalCost) + WorkflowCostResponse(result) } } @@ -26,4 +30,21 @@ class CcmGrpcImp[F[_]: Sync: Logger](pricing: GcpPricing[F]) extends CcmFs2Grpc[ BuildInfo.buildTime, BuildInfo.toString )) + + + // ToDo: REMOVE THIS WHEN WE GET THE METADATA HOOKED UP ABOVE - I'm just passing in a sample one right now so it compiles + val sampleMetaData =MetadataResponse( + List(Call( + RuntimeAttributes(CpuNumber(1), Disks(DiskName("local-disk"), DiskSize(1), DiskType("HDD")), BootDiskSizeGb(10), Preemptible(3)), + List(), + false, + true, + Region("us-central1-c"), + Status("Success"), + MachineType("us-central1-c/f1-micro"), + BackEnd("JES"), + Attempt(1))), + Instant.now, + Instant.now + ) }