Skip to content
This repository has been archived by the owner on Mar 9, 2023. It is now read-only.

Commit

Permalink
RDS functions and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Mike Heffner committed Jul 22, 2019
1 parent 374947a commit e0f96a9
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 4 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -3,4 +3,5 @@ tmp/
node_modules/
.clasprc.json
help_dialog.html
src/functions/gen/*.ts
src/functions/gen/*.ts
.vscode/
5 changes: 3 additions & 2 deletions README.md
Expand Up @@ -148,7 +148,7 @@ EBS snapshot cost is measured by the amount of stored Gigabytes using the follow

### Pricing Duration

The AWS pricing pages for EBS costs returns pricing amounts in monthly values, despite the actual billing being billed to the second. To match the EC2 functions hourly usage, the EBS cost functions in *AWS Pricing* return costs in hourly durations. This makes it easy to multiply the combined EC2 and EBS costs by 730 (hours in month) to compute a monthly cost.
The AWS pricing pages for EBS costs returns pricing amounts in monthly values, despite the actual billing being billed to the second. To match the EC2 functions hourly usage, the EBS cost functions in *AWS Pricing* return costs in hourly durations. This makes it easy to multiply the combined EC2 and EBS costs by 730 (hours in month), for example, to compute a monthly cost.

# Notes

Expand All @@ -160,7 +160,8 @@ This currently pulls data from the pricing data files used on the main EC2 prici

* Daily, Monthly, Yearly pricing
* Data transfer
* RDS
* RDS Aurora Serverless, Storage, IOPS, Aurora Global, Data Xfer
* RDS SQL Server
* Elasticache
* Upfront down-payments for Partial and All Upfront RI's, along with hourly rates
* More services as requested
Expand Down
96 changes: 95 additions & 1 deletion scripts/gen-funcs.rb
Expand Up @@ -140,6 +140,99 @@ def gen_ebs(func_dir)
f.close
end


def gen_rds(func_dir)
outfilename = 'rds_gen.ts'
outfile = File.join(func_dir, outfilename)

f = create_file(outfile)

f.write <<~EOF
import { _rds_settings, _rds_full } from "../rds";
import { RDSDbEngine } from "../../models/rds_db_engine";
EOF

engines = {
"AURORA_MYSQL" => "Aurora_Mysql",
"AURORA_POSTGRESQL" => "Aurora_Postgresql",
"MYSQL" => "Mysql",
"POSTGRESQL" => "Postgresql",
"MARIADB" => "Mariadb"
}

engines.each do |engine|
func = <<~EOF
/**
* Returns the instance price for a #{engine[1]} RDS DB instance
*
* @param settingsRange Two-column range of default EC2 instance settings
* @param instanceType Type of RDS instance
* @param region Override the region setting (optional)
* @returns price
* @customfunction
*/
export function RDS_#{engine[0].upcase}(settingsRange: Array<Array<string>>, instanceType: string, region?: string) {
return _rds_settings(settingsRange, RDSDbEngine.#{engine[1]}, instanceType, region)
}
/**
* Returns the on-demand instance price for a #{engine[1]} RDS DB instance
*
* @param instanceType Type of RDS instance
* @param region AWS region of instance
* @returns price
* @customfunction
*/
export function RDS_#{engine[0].upcase}_OD(instanceType: string, region: string) {
return _rds_full(RDSDbEngine.#{engine[1]}, instanceType, region, 'ondemand')
}
/**
* Returns the reserved instance price for a #{engine[1]} RDS DB instance
*
* @param instanceType Type of RDS instance
* @param region AWS region of instance
* @param purchaseTerm Duration of RI in years (1 or 3)
* @param paymentOption Payment terms (no_upfront, partial_upfront, all_upfront)
* @returns price
* @customfunction
*/
export function RDS_#{engine[0].upcase}_RI(instanceType: string, region: string, purchaseTerm: string | number, paymentOption: string) {
return _rds_full(RDSDbEngine.#{engine[1]}, instanceType, region, 'reserved', purchaseTerm, paymentOption)
}
EOF
f.write(func)


payment_options = {
"no_upfront" => "no",
"partial_upfront" => "partial",
"all_upfront" => "all"
}

payment_options.each do |payment_option|
func = <<~EOF
/**
* Returns the reserved instance price for a #{engine[1]} RDS DB instance with #{payment_option[0]} payment
*
* @param instanceType Type of RDS instance
* @param region AWS region of instance
* @param purchaseTerm Duration of RI in years (1 or 3)
* @returns price
* @customfunction
*/
export function RDS_#{engine[0].upcase}_RI_#{payment_option[1].upcase}(instanceType: string, region: string, purchaseTerm: string | number) {
return _rds_full(RDSDbEngine.#{engine[1]}, instanceType, region, 'reserved', purchaseTerm, "#{payment_option[0]}")
}
EOF
f.write(func)
end
end

f.close
end
#
# MAIN
#
Expand All @@ -150,4 +243,5 @@ def gen_ebs(func_dir)
func_dir = File.join(topdir, 'src/functions/gen')

gen_ec2_ri(func_dir)
gen_ebs(func_dir)
gen_ebs(func_dir)
gen_rds(func_dir)
61 changes: 61 additions & 0 deletions src/functions/rds.ts
@@ -0,0 +1,61 @@
import { RDSDbEngine } from "../models/rds_db_engine";
import { InvocationSettings } from "../settings/invocation_settings";
import { RDSPrice } from "../rds_price";
import { PriceDuration } from "../price_converter";
import { _initContext } from "../context";
import { RDSSettingsValidator } from "../settings/rds_settings_validator";

function _rds(settings: InvocationSettings, dbEngine: RDSDbEngine, instanceType: string): number {
if (dbEngine === undefined || dbEngine === null) {
throw `Must specify DB engine`
}

if (!instanceType) {
throw `Must specify a DB instance type`
}

let [ret, msg] = new RDSSettingsValidator(settings).validate()
if (!ret) {
throw msg
}

instanceType = instanceType.toString().toLowerCase()

return new RDSPrice(settings, dbEngine, instanceType).get(PriceDuration.Hourly)
}

export function _rds_full(dbEngine: RDSDbEngine, instanceType: string, region: string,
purchaseType: string, purchaseTerm?: string | number, paymentOption?: string) {
_initContext()

let settingsMap = {
'region': region,
'purchase_type': purchaseType
}

if (purchaseType === "reserved") {
settingsMap['purchase_term'] = purchaseTerm
settingsMap['payment_option'] = paymentOption
}

let settings = InvocationSettings.loadFromMap(settingsMap)

return _rds(settings, dbEngine, instanceType)
}

export function _rds_settings(settingsRange: Array<Array<string>>, dbEngine: RDSDbEngine, instanceType: string, region?: string) {
_initContext()

if (!settingsRange) {
throw `Must specify settings range`
}

let overrides = {}
if (region) {
overrides['region'] = region
}

let settings = InvocationSettings.loadFromRange(settingsRange, overrides)

return _rds(settings, dbEngine, instanceType)
}
7 changes: 7 additions & 0 deletions src/models/rds_db_engine.ts
@@ -0,0 +1,7 @@
export enum RDSDbEngine {
Aurora_Mysql,
Aurora_Postgresql,
Mysql,
Postgresql,
Mariadb
}
15 changes: 15 additions & 0 deletions src/models/rds_instance_price.ts
@@ -0,0 +1,15 @@
import { PriceDuration } from "../price_converter";

export class RDSInstancePrice {
constructor(private readonly price, private readonly isReserved: boolean) {

}

totalPrice(duration: PriceDuration): number {
if (this.isReserved) {
return parseFloat(this.price.calculatedPrice.effectiveHourlyRate.USD)
} else {
return parseFloat(this.price.price.USD)
}
}
}
131 changes: 131 additions & 0 deletions src/rds_price.ts
@@ -0,0 +1,131 @@
import { InvocationSettings } from "./settings/invocation_settings";
import { RDSDbEngine } from "./models/rds_db_engine";
import { PriceDuration } from "./price_converter";
import { ctxt } from "./context";
import { RDSInstancePrice } from "./models/rds_instance_price";

export class RDSPrice {
constructor(private readonly settings: InvocationSettings, private readonly dbEngine: RDSDbEngine,
private readonly instanceType: string) {

}

get(duration: PriceDuration): number {
let priceData = this.loadPriceData()

return priceData.totalPrice(duration)
}

private loadPriceData() {
if (this.isReserved() && this.settings.get("purchase_term") === "3" &&
this.settings.get("payment_option") === "no_upfront") {
throw `The No-Upfront payment option is not supported for 3 year RDS RIs`
}

let pricePath = Utilities.formatString("/pricing/1.0/rds/%s/%s/%sindex.json",
this.dbEngineUrlParam(), this.purchaseTypeUrlParam(), this.azUrlParam())

let body = ctxt().awsDataLoader.loadPath(pricePath)

let resp = JSON.parse(body)

let prices = this.filterPrices(resp.prices)

if (prices.length == 0) {
throw `Unable to find RDS instance ${this.instanceType} for DB engine ${this.dbEngineStr()}`
}

if (prices.length > 1) {
throw `Too many matches found for ${this.instanceType} for DB engine ${this.dbEngineStr()}`
}

return new RDSInstancePrice(prices[0], this.isReserved())
}

private filterPrices(prices) {
return prices.filter(price => {
let ret = price.attributes['aws:region'] == this.settings.get('region') &&
price.attributes['aws:rds:term'] === this.purchaseTypeAttr() &&
price.attributes['aws:rds:deploymentOption'] === 'Single-AZ' &&
price.attributes['aws:productFamily'] === 'Database Instance' &&
price.attributes['aws:rds:instanceType'] === this.instanceType
if (!ret || !this.isReserved()) {
return ret
}

return price.attributes['aws:offerTermLeaseLength'] === this.purchaseTermAttr() &&
// There are no convertible RDS RIs
price.attributes['aws:offerTermOfferingClass'] === 'standard' &&
price.attributes['aws:offerTermPurchaseOption'] === this.paymentOptionAttr()
})
}

private isReserved(): boolean {
return this.settings.get('purchase_type') === 'reserved'
}

private dbEngineUrlParam(): string {
switch (this.dbEngine) {
case RDSDbEngine.Aurora_Mysql: {
return "aurora/mysql"
}
case RDSDbEngine.Aurora_Postgresql: {
return "aurora/postgresql"
}
case RDSDbEngine.Mysql: {
return "mysql"
}
case RDSDbEngine.Mariadb: {
return "mariadb"
}
case RDSDbEngine.Postgresql: {
return "postgresql"
}
}
}

private azUrlParam(): string {
if (this.isAurora()) {
return ""
} else {
return "single-az/"
}
}

private isAurora(): boolean {
return this.dbEngine === RDSDbEngine.Aurora_Mysql || this.dbEngine === RDSDbEngine.Aurora_Postgresql
}

private purchaseTypeUrlParam(): string {
return this.isReserved() ? "reserved-instance" : "ondemand"
}

private purchaseTypeAttr(): string {
return this.isReserved() ? "reserved" : "on-demand"
}

private purchaseTermAttr(): string {
return Utilities.formatString("%syr", this.settings.get('purchase_term'))
}

private paymentOptionAttr(): string {
switch(this.settings.get('payment_option')) {
case 'no_upfront': {
return 'No Upfront'
}
case 'partial_upfront': {
return 'Partial Upfront'
}
case 'all_upfront': {
return 'All Upfront'
}
default: {
throw `Unknown payment option ${this.settings.get('payment_option')}`
}
}
}

private dbEngineStr(): string {
return RDSDbEngine[this.dbEngine]
}
}
33 changes: 33 additions & 0 deletions src/settings/rds_settings_validator.ts
@@ -0,0 +1,33 @@
import { SettingsValidator } from "./_settings_validator";
import { InvocationSettings } from "./invocation_settings";
import { SettingKeys } from "./setting_keys";

export class RDSSettingsValidator extends SettingsValidator {
constructor(private readonly settings: InvocationSettings) {
super()
}

validate(): [boolean, string] {
let reqd = [SettingKeys.Region, SettingKeys.PurchaseType]

let [ret, msg] = this.verifyOptions(reqd)
if (!ret) {
return [ret, msg]
}

let riOpts = [SettingKeys.PurchaseTerm, SettingKeys.PaymentOption]
if (this.get(SettingKeys.PurchaseType) === "reserved") {
[ret, msg] = this.verifyOptions(riOpts)
if (!ret) {
return [ret, msg]
}
}

return [true, null]
}

protected get(key: string): string {
return this.settings.get(key)
}

}

0 comments on commit e0f96a9

Please sign in to comment.