Sqlx estimated cost calculation (#925)

Adds cost estimate for the SqlX services in the Dedicated Gateway blade (queries the new FetchPrices API to retrieve price data)
This commit is contained in:
siddjoshi-ms 2021-07-22 10:48:19 -07:00 committed by GitHub
parent ed9cf01b50
commit 39a67dbc98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 7 deletions

View File

@ -40,7 +40,7 @@
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"Cost": "Cost",
"ApproximateCost": "Approximate Cost Per Hour",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",

View File

@ -4,7 +4,12 @@ import { armRequestWithoutPolling } from "../../Utils/arm/request";
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
import { RefreshResult } from "../SelfServeTypes";
import SqlX from "./SqlX";
import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes";
import {
FetchPricesResponse,
RegionsResponse,
SqlxServiceResource,
UpdateDedicatedGatewayRequestParameters,
} from "./SqlxTypes";
const apiVersion = "2021-04-01-preview";
@ -128,3 +133,67 @@ export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResu
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
}
};
const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
};
export const getReadRegions = async (): Promise<Array<string>> => {
try {
const readRegions = new Array<string>();
const response = await armRequestWithoutPolling<RegionsResponse>({
host: configContext.ARM_ENDPOINT,
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
method: "GET",
apiVersion: "2021-04-01-preview",
});
if (response.result.location !== undefined) {
readRegions.push(response.result.location.replace(" ", "").toLowerCase());
} else {
for (const location of response.result.locations) {
readRegions.push(location.locationName.replace(" ", "").toLowerCase());
}
}
return readRegions;
} catch (err) {
return new Array<string>();
}
};
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
};
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
try {
const priceMap = new Map<string, Map<string, number>>();
for (const region of regions) {
const regionPriceMap = new Map<string, number>();
const response = await armRequestWithoutPolling<FetchPricesResponse>({
host: configContext.ARM_ENDPOINT,
path: getFetchPricesPathForRegion(userContext.subscriptionId),
method: "POST",
apiVersion: "2020-01-01-preview",
queryParams: {
filter:
"armRegionNameeq '" +
region +
"'andserviceFamilyeq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
},
});
for (const item of response.result.Items) {
regionPriceMap.set(item.skuName, item.retailPrice);
}
priceMap.set(region, regionPriceMap);
}
return priceMap;
} catch (err) {
return undefined;
}
};

View File

@ -16,11 +16,13 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
import {
deleteDedicatedGatewayResource,
getCurrentProvisioningState,
getPriceMap,
getReadRegions,
refreshDedicatedGatewayProvisioning,
updateDedicatedGatewayResource,
} from "./SqlX.rp";
const costPerHourValue: Description = {
const costPerHourDefaultValue: Description = {
textTKey: "CostText",
type: DescriptionType.Text,
link: {
@ -53,7 +55,10 @@ const CosmosD16s = "Cosmos.D16s";
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
currentValues.set("sku", { value: newValue });
currentValues.set("costPerHour", { value: costPerHourValue });
currentValues.set("costPerHour", {
value: calculateCost(newValue as string, currentValues.get("instances").value as number),
});
return currentValues;
};
@ -79,6 +84,11 @@ const onNumberOfInstancesChange = (
} else {
currentValues.set("warningBanner", undefined);
}
currentValues.set("costPerHour", {
value: calculateCost(currentValues.get("sku").value as string, newValue as number),
});
return currentValues;
};
@ -111,6 +121,11 @@ const onEnableDedicatedGatewayChange = (
} as Description,
hidden: false,
});
currentValues.set("costPerHour", {
value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number),
hidden: false,
});
} else {
currentValues.set("warningBanner", {
value: {
@ -122,6 +137,8 @@ const onEnableDedicatedGatewayChange = (
} as Description,
hidden: false,
});
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
}
const sku = currentValues.get("sku");
const instances = currentValues.get("instances");
@ -137,7 +154,6 @@ const onEnableDedicatedGatewayChange = (
disabled: dedicatedGatewayOriginallyEnabled,
});
currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes });
currentValues.set("connectionString", {
value: connectionStringValue,
hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
@ -177,6 +193,40 @@ const NumberOfInstancesDropdownInfo: Info = {
},
};
const ApproximateCostDropDownInfo: Info = {
messageTKey: "CostText",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
textTKey: "DedicatedGatewayPricing",
},
};
let priceMap: Map<string, Map<string, number>>;
let regions: Array<string>;
const calculateCost = (skuName: string, instanceCount: number): Description => {
try {
let costPerHour = 0;
for (const region of regions) {
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
if (incrementalCost === undefined) {
throw new Error("Value not found in map.");
}
costPerHour += incrementalCost;
}
costPerHour *= instanceCount;
costPerHour = Math.round(costPerHour * 100) / 100;
return {
textTKey: `${costPerHour} USD`,
type: DescriptionType.Text,
};
} catch (err) {
return costPerHourDefaultValue;
}
};
@IsDisplayable()
@RefreshOptions({ retryIntervalInMs: 20000 })
export default class SqlX extends SelfServeBaseClass {
@ -274,12 +324,15 @@ export default class SqlX extends SelfServeBaseClass {
hidden: true,
});
regions = await getReadRegions();
priceMap = await getPriceMap(regions);
const response = await getCurrentProvisioningState();
if (response.status && response.status !== "Deleting") {
defaults.set("enableDedicatedGateway", { value: true });
defaults.set("sku", { value: response.sku, disabled: true });
defaults.set("instances", { value: response.instances, disabled: false });
defaults.set("costPerHour", { value: costPerHourValue });
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
defaults.set("connectionString", {
value: connectionStringValue,
hidden: false,
@ -338,8 +391,9 @@ export default class SqlX extends SelfServeBaseClass {
})
instances: number;
@PropertyInfo(ApproximateCostDropDownInfo)
@Values({
labelTKey: "Cost",
labelTKey: "ApproximateCost",
isDynamicDescription: true,
})
costPerHour: string;

View File

@ -29,3 +29,23 @@ export type UpdateDedicatedGatewayRequestProperties = {
instanceCount: number;
serviceType: string;
};
export type FetchPricesResponse = {
Items: Array<PriceItem>;
NextPageLink: string | undefined;
Count: number;
};
export type PriceItem = {
retailPrice: number;
skuName: string;
};
export type RegionsResponse = {
locations: Array<RegionItem>;
location: string;
};
export type RegionItem = {
locationName: string;
};