From d7718c42da711cd968d832b58604cddc99f8f339 Mon Sep 17 00:00:00 2001 From: Tara Zou Date: Wed, 24 Apr 2024 18:51:09 -0400 Subject: [PATCH] upapi update --- src/Common/Constants.ts | 1 + src/ConfigContext.ts | 7 +++ src/Contracts/ViewModels.ts | 1 + src/SelfServe/SelfServe.tsx | 7 ++- src/SelfServe/SqlX/SqlX.rp.ts | 87 ++++++++++++++++++++++++++------- src/SelfServe/SqlX/SqlxTypes.ts | 41 +++++++++++++--- src/Utils/arm/request.ts | 55 +++++++++++++++++++++ 7 files changed, 172 insertions(+), 27 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index fe9c2672d..33be4426f 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -248,6 +248,7 @@ export class HttpHeaders { public static partitionKey: string = "x-ms-documentdb-partitionkey"; public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput"; public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; + public static xAPIKey: string = "X-API-Key"; } export class ContentType { diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index e3350c7f6..64e53dc9c 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -42,6 +42,9 @@ export interface ConfigContext { ARM_API_VERSION: string; GRAPH_ENDPOINT: string; GRAPH_API_VERSION: string; + CATALOG_ENDPOINT: string; + CATALOG_API_VERSION: string; + CATALOG_API_KEY: string; ARCADIA_ENDPOINT: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; BACKEND_ENDPOINT?: string; @@ -92,6 +95,9 @@ let configContext: Readonly = { ARM_API_VERSION: "2016-06-01", GRAPH_ENDPOINT: "https://graph.microsoft.com", GRAPH_API_VERSION: "1.6", + CATALOG_ENDPOINT: "https://catalogapi.azure.com/", + CATALOG_API_VERSION: "2023-05-01-preview", + CATALOG_API_KEY: "", ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 @@ -248,3 +254,4 @@ export async function initializeConfiguration(): Promise { } export { configContext }; + diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index b79ddce4e..2de59298b 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -419,6 +419,7 @@ export interface SelfServeFrameInputs { authorizationToken: string; csmEndpoint: string; flights?: readonly string[]; + catalogAPIKey: string; } export class MonacoEditorSettings { diff --git a/src/SelfServe/SelfServe.tsx b/src/SelfServe/SelfServe.tsx index f663a3ef9..62f57931a 100644 --- a/src/SelfServe/SelfServe.tsx +++ b/src/SelfServe/SelfServe.tsx @@ -97,6 +97,9 @@ const handleMessage = async (event: MessageEvent): Promise => { } const inputs = event.data.data.inputs as SelfServeFrameInputs; + // Test + console.log("catalogAPIKey" + inputs.catalogAPIKey); + // End Test if (!inputs) { return; } @@ -110,13 +113,15 @@ const handleMessage = async (event: MessageEvent): Promise => { !inputs.databaseAccount || !inputs.authorizationToken || !inputs.csmEndpoint || - !selfServeType + !selfServeType || + !inputs.catalogAPIKey ) { return; } updateConfigContext({ ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), + CATALOG_API_KEY: inputs.catalogAPIKey, }); updateUserContext({ diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index 9cb430971..945765cd0 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -1,11 +1,14 @@ import { configContext } from "../../ConfigContext"; import { userContext } from "../../UserContext"; -import { armRequestWithoutPolling } from "../../Utils/arm/request"; +import { armRequestWithoutPolling, getOfferingIdsRequest } from "../../Utils/arm/request"; import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; import { RefreshResult } from "../SelfServeTypes"; import SqlX from "./SqlX"; import { FetchPricesResponse, + GetOfferingIdsResponse, + OfferingIdMap, + OfferingIdRequest, PriceMapAndCurrencyCode, RegionItem, RegionsResponse, @@ -170,7 +173,7 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => { return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; }; -export const getPriceMapAndCurrencyCode = async (regions: Array): Promise => { +export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise => { const telemetryData = { feature: "Calculate approximate cost", function: "getPriceMapAndCurrencyCode", @@ -181,39 +184,85 @@ export const getPriceMapAndCurrencyCode = async (regions: Array): Pr try { const priceMap = new Map>(); - let currencyCode; - for (const regionItem of regions) { + let pricingCurrency; + for (const region of map.keys()) { const regionPriceMap = new Map(); + const requestBody: OfferingIdRequest = { + location: region, + ids: Array.from(map.get(region).values()), + }; const response = await armRequestWithoutPolling({ host: configContext.ARM_ENDPOINT, path: getFetchPricesPathForRegion(userContext.subscriptionId), method: "POST", - apiVersion: "2020-01-01-preview", + apiVersion: "2023-04-01-preview", + body: requestBody, + }); + + for (const item of response.result.Items) { + if (pricingCurrency === undefined) { + pricingCurrency = item.pricingCurrency; + } else if (item.pricingCurrency !== pricingCurrency) { + throw Error("Currency Code Mismatch: Currency code not same for all regions / skus."); + } + + const offeringId = item.id; + const skuName = map.get(region).get(offeringId); + const unitPrice = item.prices.find(x => x.type == "Consumption").unitPrice; + regionPriceMap.set(skuName, unitPrice); + } + priceMap.set(region, regionPriceMap); + } + + selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp); + return { priceMap: priceMap, pricingCurrency: pricingCurrency }; + } catch (err) { + const failureTelemetry = { err, selfServeClassName: SqlX.name }; + selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp); + return { priceMap: undefined, pricingCurrency: undefined }; + } +}; + +export const getOfferingIds = async (regions: Array): Promise => { + const telemetryData = { + feature: "Get Offering Ids to calculate approximate cost", + function: "getOfferingIds", + description: "fetch offering ids API call", + selfServeClassName: SqlX.name, + }; + const getOfferingIdsCodeTimestamp = selfServeTraceStart(telemetryData); + + try { + const offeringIdMap = new Map>(); + let currencyCode; + for (const regionItem of regions) { + const regionOfferingIdMap = new Map(); + + const response = await getOfferingIdsRequest({ + host: configContext.CATALOG_ENDPOINT, + path: `/skus`, + method: "GET", + apiVersion: "2023-05-01-preview", queryParams: { filter: - "armRegionName eq '" + - regionItem.locationName.split(" ").join("").toLowerCase() + - "' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'", + "locations eq '" + + regionItem.locationName + + "' and serviceFamily eq 'Databases' and service eq 'Azure Cosmos DB'", }, }); for (const item of response.result.Items) { - if (currencyCode === undefined) { - currencyCode = item.currencyCode; - } else if (item.currencyCode !== currencyCode) { - throw Error("Currency Code Mismatch: Currency code not same for all regions / skus."); - } - regionPriceMap.set(item.skuName, item.retailPrice); + regionOfferingIdMap.set(item.skuName, item.offeringProperties.offeringId); } - priceMap.set(regionItem.locationName, regionPriceMap); + offeringIdMap.set(regionItem.locationName, regionOfferingIdMap); } - selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp); - return { priceMap: priceMap, currencyCode: currencyCode }; + selfServeTraceSuccess(telemetryData, getOfferingIdsCodeTimestamp); + return offeringIdMap; } catch (err) { const failureTelemetry = { err, selfServeClassName: SqlX.name }; - selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp); - return { priceMap: undefined, currencyCode: undefined }; + selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp); + return undefined; } }; diff --git a/src/SelfServe/SqlX/SqlxTypes.ts b/src/SelfServe/SqlX/SqlxTypes.ts index 3360df734..826061b71 100644 --- a/src/SelfServe/SqlX/SqlxTypes.ts +++ b/src/SelfServe/SqlX/SqlxTypes.ts @@ -36,17 +36,44 @@ export type FetchPricesResponse = { Count: number; }; -export type PriceMapAndCurrencyCode = { - priceMap: Map>; - currencyCode: string; +export type PriceItem = { + prices: Array; + id: string; + pricingCurrency: string; }; -export type PriceItem = { - retailPrice: number; - skuName: string; - currencyCode: string; +export type PriceType = { + type: string; + unitPrice: number; +} + +export type PriceMapAndCurrencyCode = { + priceMap: Map>; + pricingCurrency: string; }; +export type GetOfferingIdsResponse = { + Items: Array; + NextPageLink: string | undefined; + Count: number; +}; + +export type OfferingIdItem = { + skuName: string; + offeringProperties: OfferingProperties; +}; + +export type OfferingProperties = { + offeringId: string; +} + +export type OfferingIdRequest = { + ids: Array; + location: string, +} + +export type OfferingIdMap = Map>; + export type RegionsResponse = { properties: RegionsProperties; }; diff --git a/src/Utils/arm/request.ts b/src/Utils/arm/request.ts index 69391201b..bd704050e 100644 --- a/src/Utils/arm/request.ts +++ b/src/Utils/arm/request.ts @@ -160,3 +160,58 @@ async function getOperationStatus(operationStatusUrl: string) { } throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`); } + +export async function getOfferingIdsRequest({ + host, + path, + apiVersion, + method, + body: requestBody, + queryParams, + contentType, +}: Options): Promise<{ result: T; operationStatusUrl: string }> { + const url = new URL(path, host); + url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); + if (queryParams) { + queryParams.filter && url.searchParams.append("$filter", queryParams.filter); + queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames); + } + + if (!configContext.CATALOG_API_KEY) { + throw new Error("No catalog API key provided"); + } + + // TODO: delete after test + // console.log("config CATALOG_API_KEY: " + configContext.CATALOG_API_KEY); + // End Test + + const response = await window.fetch(url.href, { + method, + headers: { + // [HttpHeaders.xAPIKey]: configContext.CATALOG_API_KEY, + [HttpHeaders.xAPIKey]: "", + }, + body: requestBody ? JSON.stringify(requestBody) : undefined, + }); + if (!response.ok) { + let error: ARMError; + try { + const errorResponse = (await response.json()) as ParsedErrorResponse; + if ("error" in errorResponse) { + error = new ARMError(errorResponse.error.message); + error.code = errorResponse.error.code; + } else { + error = new ARMError(errorResponse.message); + error.code = errorResponse.code; + } + } catch (error) { + throw new Error(await response.text()); + } + + throw error; + } + + const operationStatusUrl = (response.headers && response.headers.get("location")) || ""; + const responseBody = (await response.json()) as T; + return { result: responseBody, operationStatusUrl: operationStatusUrl }; +}