From 547faf4d4e8644900db42063aff380a84bee1a67 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Tue, 23 Jan 2024 16:53:19 -0800 Subject: [PATCH 01/17] Initial code --- src/cogs/handler.js | 139 +++++++++++++++++++++++++++++++++++++++ test/audits/cogs.test.js | 0 2 files changed, 139 insertions(+) create mode 100644 src/cogs/handler.js create mode 100644 test/audits/cogs.test.js diff --git a/src/cogs/handler.js b/src/cogs/handler.js new file mode 100644 index 00000000..30109213 --- /dev/null +++ b/src/cogs/handler.js @@ -0,0 +1,139 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import AWSCostApiClient from '@adobe/spacecat-shared-aws-api-client'; + + + +function calculatePreviousMonthDate() { + const date = new Date(); + let month = 1 + date.getMonth(); // as month starts from 0 + let year = date.getFullYear(); + const endDate = `${String(year).padStart(4, '0')}/${String(month).padStart(2, '0')}/01`; + if (month === 1) { // if month is january then previous month will be december of previous year + month = 12; + year -= 1; + } + else { + month -= 1; + } + const startDate = `${String(year).padStart(4, '0')}/${String(month).padStart(2, '0')}/01`; + return { startDate, endDate }; +} +const { startDate, endDate } = calculatePreviousMonthDate(); +const input = { + "TimePeriod": { + "End": endDate, + "Start": startDate + }, + "Granularity": "MONTHLY", + "Filter": { + "Tags": { + "Key": 'Adobe.ArchPath', + "Values": ['EC.SpaceCat.Services'], + "MatchOptions": ['EQUALS'] + } + }, + "Metrics": [ + "UnblendedCost" + ], + "GroupBy": [ + { + "Key": "SERVICE", + "Type": "DIMENSION" + }, + { + "Key": "Environment", + "Type": "TAG" + } + ] +}; + +const parseYearDate = (str) => { + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const date = new Date(str); + return `${months[date.getMonth()]}-${date.getFullYear().toString().substring(2)}`; +}; + +const serviceMap = (service) => { + switch (service) { + case 'AWS Lambda': + return 'LAMBDA'; + case 'AWS Secrets Manager': + return 'SECRETS MANAGER'; + case 'Amazon DynamoDB': + return 'DYNAMODB'; + case 'Amazon Simple Storage Service': + return 'S3'; + case 'Amazon Simple Queue Service': + return 'SQS'; + case 'AmazonCloudWatch': + return 'CLOUDWATCH'; + default: + return service; + } +}; + + +/** + * url param in run-query@v3/rum-dashboard works in a 'startsWith' fashion. url=domain.com returns + * an empty result whereas url=www.domain.com/ returns the desired result. To catch the redirects + * to subdomains we issue a GET call to the domain, then use the final url after redirects + * @param url + * @returns finalUrl {Promise} + */ + +function processAWSResponse(data) { + if (data && data.ResultsByTime && data.ResultsByTime.length > 0) { + let result; + data.ResultsByTime.forEach((result) => { + if (result.Groups && result.Groups.length > 0) { + let granularity; + result.Groups.forEach((group) => { + const key = group.Keys[0]; + if (group.Metrics && group.Metrics.UnblendedCost) { + granularity[serviceMap(key)] = parseFloat(group.Metrics.UnblendedCost.Amount).toFixed(2); + } + }); + result[parseYearDate(result.TimePeriod.Start)] = granularity; + } + }); + return result; + } +} +export default async function auditCOGs(message, context) { + const { type, url, auditContext } = message; + const { log, sqs } = context; + const { + AUDIT_RESULTS_QUEUE_URL: queueUrl, + } = context.env; + try { + log.info(`Received audit req for domain: ${url}`); + + const awsAPIClient = AWSCostApiClient.createFrom(context); + + const data = await awsAPIClient.getUsageCost(input); + const auditResult = processAWSResponse(data); + + await sqs.sendMessage(queueUrl, { + type, + url, + auditContext, + auditResult, + }); + + log.info(`Successfully audited ${url} for ${type} type audit`); + return noContent(); + } catch (e) { + return internalServerError(`Internal server error: ${e.message}`); + } +} diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js new file mode 100644 index 00000000..e69de29b From 7c71baa241abdd23d3ff5d4c169c5a7982bac75a Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Thu, 25 Jan 2024 17:00:56 -0800 Subject: [PATCH 02/17] Adding cogs codebase --- package-lock.json | 904 +++++++++++++++++++++++++++++---------- package.json | 5 +- src/cogs/handler.js | 154 ++++--- src/index.js | 2 + test/audits/cogs.test.js | 47 ++ 5 files changed, 828 insertions(+), 284 deletions(-) diff --git a/package-lock.json b/package-lock.json index d0075eb0..7ac79d7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@adobe/spacecat-shared-http-utils": "1.1.1", "@adobe/spacecat-shared-rum-api-client": "1.3.2", "@adobe/spacecat-shared-utils": "1.7.1", + "@aws-sdk/client-cost-explorer": "^3.499.0", "@aws-sdk/client-sqs": "3.490.0", "diff": "5.1.0", "urijs": "1.19.11" @@ -1807,6 +1808,465 @@ } } }, + "node_modules/@aws-sdk/client-cost-explorer": { + "version": "3.499.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cost-explorer/-/client-cost-explorer-3.499.0.tgz", + "integrity": "sha512-FCT1OJsPkzz+SKKm/8XdCAWLBb/CYSFYHKI87dEzfyHek8pBhH+iJpaK9XCfPQXufTxaO+NEkib82SSRlnVpUw==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.499.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/credential-provider-node": "3.499.0", + "@aws-sdk/middleware-host-header": "3.496.0", + "@aws-sdk/middleware-logger": "3.496.0", + "@aws-sdk/middleware-recursion-detection": "3.496.0", + "@aws-sdk/middleware-signing": "3.496.0", + "@aws-sdk/middleware-user-agent": "3.496.0", + "@aws-sdk/region-config-resolver": "3.496.0", + "@aws-sdk/types": "3.496.0", + "@aws-sdk/util-endpoints": "3.496.0", + "@aws-sdk/util-user-agent-browser": "3.496.0", + "@aws-sdk/util-user-agent-node": "3.496.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/client-sso": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.496.0.tgz", + "integrity": "sha512-fuaMuxKg7CMUsP9l3kxYWCOxFsBjdA0xj5nlikaDm1661/gB4KkAiGqRY8LsQkpNXvXU8Nj+f7oCFADFyGYzyw==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.496.0", + "@aws-sdk/middleware-logger": "3.496.0", + "@aws-sdk/middleware-recursion-detection": "3.496.0", + "@aws-sdk/middleware-user-agent": "3.496.0", + "@aws-sdk/region-config-resolver": "3.496.0", + "@aws-sdk/types": "3.496.0", + "@aws-sdk/util-endpoints": "3.496.0", + "@aws-sdk/util-user-agent-browser": "3.496.0", + "@aws-sdk/util-user-agent-node": "3.496.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/client-sts": { + "version": "3.499.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.499.0.tgz", + "integrity": "sha512-Eyj9STw2DXMtXL5V/v0HYHO6+JjGPi257M5IYyxwqlvRchq6jbOsedobfxclB/gBUyBRtZdnyAIS8uCKjb4kpA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/credential-provider-node": "3.499.0", + "@aws-sdk/middleware-host-header": "3.496.0", + "@aws-sdk/middleware-logger": "3.496.0", + "@aws-sdk/middleware-recursion-detection": "3.496.0", + "@aws-sdk/middleware-user-agent": "3.496.0", + "@aws-sdk/region-config-resolver": "3.496.0", + "@aws-sdk/types": "3.496.0", + "@aws-sdk/util-endpoints": "3.496.0", + "@aws-sdk/util-user-agent-browser": "3.496.0", + "@aws-sdk/util-user-agent-node": "3.496.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/core": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.496.0.tgz", + "integrity": "sha512-yT+ug7Cw/3eJi7x2es0+46x12+cIJm5Xv+GPWsrTFD1TKgqO/VPEgfDtHFagDNbFmjNQA65Ygc/kEdIX9ICX/A==", + "dependencies": { + "@smithy/core": "^1.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/signature-v4": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.496.0.tgz", + "integrity": "sha512-lukQMJ8SWWP5RqkRNOHi/H+WMhRvSWa3Fc5Jf/VP6xHiPLfF1XafcvthtV91e0VwPCiseI+HqChrcGq8pvnxHw==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.496.0.tgz", + "integrity": "sha512-2nD1jp1sIwcQaWK1y/9ruQOkW16RUxZpzgjbW/gnK3iiUXwx+/FNQWxshud+GTSx3Q4x6eIhqsbjtP4VVPPuUA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.496.0", + "@aws-sdk/credential-provider-process": "3.496.0", + "@aws-sdk/credential-provider-sso": "3.496.0", + "@aws-sdk/credential-provider-web-identity": "3.496.0", + "@aws-sdk/types": "3.496.0", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.499.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.499.0.tgz", + "integrity": "sha512-EsiSevVmcVSMIq7D9siSH/XVc5I0vMntg1rx6KQdng1Fq8X/RBL5t9wSWEwOl7KFo5HlEsWrLWIpo1WHuzIL/w==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.496.0", + "@aws-sdk/credential-provider-ini": "3.496.0", + "@aws-sdk/credential-provider-process": "3.496.0", + "@aws-sdk/credential-provider-sso": "3.496.0", + "@aws-sdk/credential-provider-web-identity": "3.496.0", + "@aws-sdk/types": "3.496.0", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.496.0.tgz", + "integrity": "sha512-/YZscCTGOKVmGr916Th4XF8Sz6JDtZ/n2loHG9exok9iy/qIbACsTRNLP9zexPxhPoue/oZqecY5xbVljfY34A==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.496.0.tgz", + "integrity": "sha512-eP7GxpT2QYubSDG7uk1GJW4eNymZCq65IxDyEFCXOP/kfqkxriCY+iVEFG6/Mo3LxvgrgHXU4jxrCAXMAWN43g==", + "dependencies": { + "@aws-sdk/client-sso": "3.496.0", + "@aws-sdk/token-providers": "3.496.0", + "@aws-sdk/types": "3.496.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.496.0.tgz", + "integrity": "sha512-IbP+qLlvJSpNPj+zW6TtFuLRTK5Tf0hW+2pom4vFyi5YSH4pn8UOC136UdewX8vhXGS9BJQ5zBDMasIyl5VeGQ==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.496.0.tgz", + "integrity": "sha512-jUdPpSJeqCYXf6hSjfwsfHway7peIV8Vz51w/BN91bF4vB/bYwAC5o9/iJiK/EoByp5asxA8fg9wFOyGjzdbLg==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/middleware-logger": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.496.0.tgz", + "integrity": "sha512-EwMVSY6iBMeGbVnvwdaFl/ClMS/YWtxCAo+bcEtgk8ltRuo7qgbJem8Km/fvWC1vdWvIbe4ArdJ8iGzq62ffAw==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.496.0.tgz", + "integrity": "sha512-+IuOcFsfqg2WAnaEzH6KhVbicqCxtOq9w3DH2jwTpddRlCx2Kqf6wCzg8luhHRGyjBZdsbIS+OXwyMevoppawA==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/middleware-signing": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.496.0.tgz", + "integrity": "sha512-Oq73Brs4IConvWnRlh8jM1V7LHoTw9SVQklu/QW2FPlNrB3B8fuTdWHHYIWv7ybw1bykXoCY99v865Mmq/Or/g==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/signature-v4": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-middleware": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.496.0.tgz", + "integrity": "sha512-+iMtRxFk0GmFWNUF4ilxylOQd9PZdR4ZC9jkcPIh1PZlvKtpCyFywKlk5RRZKklSoJ/CttcqwhMvOXTNbWm/0w==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@aws-sdk/util-endpoints": "3.496.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.496.0.tgz", + "integrity": "sha512-URrNVOPHPgEDm6QFu6lDC2cUFs+Jx23mA3jEwCvoKlXiEY/ZoWjH8wlX3OMUlLrF1qoUTuD03jjrJzF6zoCgug==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/token-providers": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.496.0.tgz", + "integrity": "sha512-fyi8RcObEa1jNETJdc2H6q9VHrrdKCj/b6+fbLvymb7mUVRd0aWUn+24SNUImnSOnrwYnwaMfyyEC388X4MbFQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.496.0", + "@aws-sdk/middleware-logger": "3.496.0", + "@aws-sdk/middleware-recursion-detection": "3.496.0", + "@aws-sdk/middleware-user-agent": "3.496.0", + "@aws-sdk/region-config-resolver": "3.496.0", + "@aws-sdk/types": "3.496.0", + "@aws-sdk/util-endpoints": "3.496.0", + "@aws-sdk/util-user-agent-browser": "3.496.0", + "@aws-sdk/util-user-agent-node": "3.496.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/types": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.496.0.tgz", + "integrity": "sha512-umkGadK4QuNQaMoDICMm7NKRI/mYSXiyPjcn3d53BhsuArYU/52CebGQKdt4At7SwwsiVJZw9RNBHyN5Mm0HVw==", + "dependencies": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/util-endpoints": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.496.0.tgz", + "integrity": "sha512-1QzOiWHi383ZwqSi/R2KgKCd7M+6DxkxI5acqLPm8mvDRDP2jRjrnVaC0g9/tlttWousGEemDUWStwrD2mVYSw==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/types": "^2.9.1", + "@smithy/util-endpoints": "^1.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.496.0.tgz", + "integrity": "sha512-4j2spN+h0I0qfSMsGvJXTfQBu1e18rPdekKvzsGJxhaAE1tNgUfUT4nbvc5uVn0sNjZmirskmJ3kfbzVOrqIFg==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/types": "^2.9.1", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/client-cost-explorer/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.496.0.tgz", + "integrity": "sha512-h0Ax0jlDc7UIo3KoSI4C4tVLBFoiAdx3+DhTVfgLS7x93d41dMlziPoBX2RgdcFn37qnzw6AQKTVTMwDbRCGpg==", + "dependencies": { + "@aws-sdk/types": "3.496.0", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.465.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.465.0.tgz", @@ -7454,11 +7914,11 @@ "dev": true }, "node_modules/@smithy/abort-controller": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.16.tgz", - "integrity": "sha512-4foO7738k8kM9flMHu3VLabqu7nPgvIj8TB909S0CnKx0YZz/dcDH3pZ/4JHdatfxlZdKF1JWOYCw9+v3HVVsw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.1.tgz", + "integrity": "sha512-1+qdrUqLhaALYL0iOcN43EP6yAXXQ2wWZ6taf4S2pNGowmOc5gx+iMQv+E42JizNJjB0+gEadOXeV1Bf7JWL1Q==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7485,14 +7945,14 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.0.23.tgz", - "integrity": "sha512-XakUqgtP2YY8Mi+Nlif5BiqJgWdvfxJafSpOSQeCOMizu+PUhE4fBQSy6xFcR+eInrwVadaABNxoJyGUMn15ew==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.1.tgz", + "integrity": "sha512-lxfLDpZm+AWAHPFZps5JfDoO9Ux1764fOgvRUBpHIO8HWHcSN1dkgsago1qLRVgm1BZ8RCm8cgv99QvtaOWIhw==", "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/types": "^2.8.0", - "@smithy/util-config-provider": "^2.1.0", - "@smithy/util-middleware": "^2.0.9", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7500,17 +7960,17 @@ } }, "node_modules/@smithy/core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.2.2.tgz", - "integrity": "sha512-uLjrskLT+mWb0emTR5QaiAIxVEU7ndpptDaVDrTwwhD+RjvHhjIiGQ3YL5jKk1a5VSDQUA2RGkXvJ6XKRcz6Dg==", - "dependencies": { - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/protocol-http": "^3.0.12", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/util-middleware": "^2.0.9", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.1.tgz", + "integrity": "sha512-tf+NIu9FkOh312b6M9G4D68is4Xr7qptzaZGZUREELF8ysE1yLKphqt7nsomjKZVwW7WE5pDDex9idowNGRQ/Q==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-middleware": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7518,14 +7978,14 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.1.5.tgz", - "integrity": "sha512-VfvE6Wg1MUWwpTZFBnUD7zxvPhLY8jlHCzu6bCjlIYoWgXCDzZAML76IlZUEf45nib3rjehnFgg0s1rgsuN/bg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.1.tgz", + "integrity": "sha512-7XHjZUxmZYnONheVQL7j5zvZXga+EWNgwEAP6OPZTi7l8J4JTeNh9aIOfE5fKHZ/ee2IeNOh54ZrSna+Vc6TFA==", "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/property-provider": "^2.0.17", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7533,13 +7993,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.0.16.tgz", - "integrity": "sha512-umYh5pdCE9GHgiMAH49zu9wXWZKNHHdKPm/lK22WYISTjqu29SepmpWNmPiBLy/yUu4HFEGJHIFrDWhbDlApaw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.1.tgz", + "integrity": "sha512-E8KYBxBIuU4c+zrpR22VsVrOPoEDzk35bQR3E+xm4k6Pa6JqzkDOdMyf9Atac5GPNKHJBdVaQ4JtjdWX2rl/nw==", "dependencies": { "@aws-crypto/crc32": "3.0.0", - "@smithy/types": "^2.8.0", - "@smithy/util-hex-encoding": "^2.0.0", + "@smithy/types": "^2.9.1", + "@smithy/util-hex-encoding": "^2.1.1", "tslib": "^2.5.0" } }, @@ -7599,14 +8059,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.3.2.tgz", - "integrity": "sha512-O9R/OlnAOTsnysuSDjt0v2q6DcSvCz5cCFC/CFAWWcLyBwJDeFyGTCTszgpQTb19+Fi8uRwZE5/3ziAQBFeDMQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.1.tgz", + "integrity": "sha512-VYGLinPsFqH68lxfRhjQaSkjXM7JysUOJDTNjHBuN/ykyRb2f1gyavN9+VhhPTWCy32L4yZ2fdhpCs/nStEicg==", "dependencies": { - "@smithy/protocol-http": "^3.0.12", - "@smithy/querystring-builder": "^2.0.16", - "@smithy/types": "^2.8.0", - "@smithy/util-base64": "^2.0.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/querystring-builder": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-base64": "^2.1.1", "tslib": "^2.5.0" } }, @@ -7623,13 +8083,13 @@ } }, "node_modules/@smithy/hash-node": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.0.18.tgz", - "integrity": "sha512-gN2JFvAgnZCyDN9rJgcejfpK0uPPJrSortVVVVWsru9whS7eQey6+gj2eM5ln2i6rHNntIXzal1Fm9XOPuoaKA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.1.tgz", + "integrity": "sha512-Qhoq0N8f2OtCnvUpCf+g1vSyhYQrZjhSwvJ9qvR8BUGOtTXiyv2x1OD2e6jVGmlpC4E4ax1USHoyGfV9JFsACg==", "dependencies": { - "@smithy/types": "^2.8.0", - "@smithy/util-buffer-from": "^2.0.0", - "@smithy/util-utf8": "^2.0.2", + "@smithy/types": "^2.9.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7651,18 +8111,18 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.0.16.tgz", - "integrity": "sha512-apEHakT/kmpNo1VFHP4W/cjfeP9U0x5qvfsLJubgp7UM/gq4qYp0GbqdE7QhsjUaYvEnrftRqs7+YrtWreV0wA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.1.tgz", + "integrity": "sha512-7WTgnKw+VPg8fxu2v9AlNOQ5yaz6RA54zOVB4f6vQuR0xFKd+RzlCpt0WidYTsye7F+FYDIaS/RnJW4pxjNInw==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" } }, "node_modules/@smithy/is-array-buffer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz", - "integrity": "sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz", + "integrity": "sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ==", "dependencies": { "tslib": "^2.5.0" }, @@ -7681,12 +8141,12 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.0.18.tgz", - "integrity": "sha512-ZJ9uKPTfxYheTKSKYB+GCvcj+izw9WGzRLhjn8n254q0jWLojUzn7Vw0l4R/Gq7Wdpf/qmk/ptD+6CCXHNVCaw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.1.tgz", + "integrity": "sha512-rSr9ezUl9qMgiJR0UVtVOGEZElMdGFyl8FzWEF5iEKTlcWxGr2wTqGfDwtH3LAB7h+FPkxqv4ZU4cpuCN9Kf/g==", "dependencies": { - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7694,16 +8154,16 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.3.0.tgz", - "integrity": "sha512-VsOAG2YQ8ykjSmKO+CIXdJBIWFo6AAvG6Iw95BakBTqk66/4BI7XyqLevoNSq/lZ6NgZv24sLmrcIN+fLDWBCg==", - "dependencies": { - "@smithy/middleware-serde": "^2.0.16", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/shared-ini-file-loader": "^2.2.8", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-middleware": "^2.0.9", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.1.tgz", + "integrity": "sha512-XPZTb1E2Oav60Ven3n2PFx+rX9EDsU/jSTA8VDamt7FXks67ekjPY/XrmmPDQaFJOTUHJNKjd8+kZxVO5Ael4Q==", + "dependencies": { + "@smithy/middleware-serde": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-middleware": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7711,17 +8171,17 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.0.26.tgz", - "integrity": "sha512-Qzpxo0U5jfNiq9iD38U3e2bheXwvTEX4eue9xruIvEgh+UKq6dKuGqcB66oBDV7TD/mfoJi9Q/VmaiqwWbEp7A==", - "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/protocol-http": "^3.0.12", - "@smithy/service-error-classification": "^2.0.9", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/util-middleware": "^2.0.9", - "@smithy/util-retry": "^2.0.9", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.1.tgz", + "integrity": "sha512-eMIHOBTXro6JZ+WWzZWd/8fS8ht5nS5KDQjzhNMHNRcG5FkNTqcKpYhw7TETMYzbLfhO5FYghHy1vqDWM4FLDA==", + "dependencies": { + "@smithy/node-config-provider": "^2.2.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/service-error-classification": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", "tslib": "^2.5.0", "uuid": "^8.3.2" }, @@ -7730,11 +8190,11 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.0.16.tgz", - "integrity": "sha512-5EAd4t30pcc4M8TSSGq7q/x5IKrxfXR5+SrU4bgxNy7RPHQo2PSWBUco9C+D9Tfqp/JZvprRpK42dnupZafk2g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.1.tgz", + "integrity": "sha512-D8Gq0aQBeE1pxf3cjWVkRr2W54t+cdM2zx78tNrVhqrDykRA7asq8yVJij1u5NDtKzKqzBSPYh7iW0svUKg76g==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7742,11 +8202,11 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.0.10.tgz", - "integrity": "sha512-I2rbxctNq9FAPPEcuA1ntZxkTKOPQFy7YBPOaD/MLg1zCvzv21CoNxR0py6J8ZVC35l4qE4nhxB0f7TF5/+Ldw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.1.tgz", + "integrity": "sha512-KPJhRlhsl8CjgGXK/DoDcrFGfAqoqvuwlbxy+uOO4g2Azn1dhH+GVfC3RAp+6PoL5PWPb+vt6Z23FP+Mr6qeCw==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7754,13 +8214,13 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.1.9.tgz", - "integrity": "sha512-tUyW/9xrRy+s7RXkmQhgYkAPMpTIF8izK4orhHjNFEKR3QZiOCbWB546Y8iB/Fpbm3O9+q0Af9rpywLKJOwtaQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.1.tgz", + "integrity": "sha512-epzK3x1xNxA9oJgHQ5nz+2j6DsJKdHfieb+YgJ7ATWxzNcB7Hc+Uya2TUck5MicOPhDV8HZImND7ZOecVr+OWg==", "dependencies": { - "@smithy/property-provider": "^2.0.17", - "@smithy/shared-ini-file-loader": "^2.2.8", - "@smithy/types": "^2.8.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7768,14 +8228,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.2.2.tgz", - "integrity": "sha512-XO58TO/Eul/IBQKFKaaBtXJi0ItEQQCT+NI4IiKHCY/4KtqaUT6y/wC1EvDqlA9cP7Dyjdj7FdPs4DyynH3u7g==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.3.1.tgz", + "integrity": "sha512-gLA8qK2nL9J0Rk/WEZSvgin4AppvuCYRYg61dcUo/uKxvMZsMInL5I5ZdJTogOvdfVug3N2dgI5ffcUfS4S9PA==", "dependencies": { - "@smithy/abort-controller": "^2.0.16", - "@smithy/protocol-http": "^3.0.12", - "@smithy/querystring-builder": "^2.0.16", - "@smithy/types": "^2.8.0", + "@smithy/abort-controller": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/querystring-builder": "^2.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7783,11 +8243,11 @@ } }, "node_modules/@smithy/property-provider": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.0.17.tgz", - "integrity": "sha512-+VkeZbVu7qtQ2DjI48Qwaf9fPOr3gZIwxQpuLJgRRSkWsdSvmaTCxI3gzRFKePB63Ts9r4yjn4HkxSCSkdWmcQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.1.tgz", + "integrity": "sha512-FX7JhhD/o5HwSwg6GLK9zxrMUrGnb3PzNBrcthqHKBc3dH0UfgEAU24xnJ8F0uow5mj17UeBEOI6o3CF2k7Mhw==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7795,11 +8255,11 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.0.12.tgz", - "integrity": "sha512-Xz4iaqLiaBfbQpB9Hgi3VcZYbP7xRDXYhd8XWChh4v94uw7qwmvlxdU5yxzfm6ACJM66phHrTbS5TVvj5uQ72w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.1.1.tgz", + "integrity": "sha512-6ZRTSsaXuSL9++qEwH851hJjUA0OgXdQFCs+VDw4tGH256jQ3TjYY/i34N4vd24RV3nrjNsgd1yhb57uMoKbzQ==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7807,12 +8267,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.0.16.tgz", - "integrity": "sha512-Q/GsJT0C0mijXMRs7YhZLLCP5FcuC4797lYjKQkME5CZohnLC4bEhylAd2QcD3gbMKNjCw8+T2I27WKiV/wToA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.1.tgz", + "integrity": "sha512-C/ko/CeEa8jdYE4gt6nHO5XDrlSJ3vdCG0ZAc6nD5ZIE7LBp0jCx4qoqp7eoutBu7VrGMXERSRoPqwi1WjCPbg==", "dependencies": { - "@smithy/types": "^2.8.0", - "@smithy/util-uri-escape": "^2.0.0", + "@smithy/types": "^2.9.1", + "@smithy/util-uri-escape": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7820,11 +8280,11 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.16.tgz", - "integrity": "sha512-c4ueAuL6BDYKWpkubjrQthZKoC3L5kql5O++ovekNxiexRXTlLIVlCR4q3KziOktLIw66EU9SQljPXd/oN6Okg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.1.tgz", + "integrity": "sha512-H4+6jKGVhG1W4CIxfBaSsbm98lOO88tpDWmZLgkJpt8Zkk/+uG0FmmqMuCAc3HNM2ZDV+JbErxr0l5BcuIf/XQ==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7832,22 +8292,22 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.0.9.tgz", - "integrity": "sha512-0K+8GvtwI7VkGmmInPydM2XZyBfIqLIbfR7mDQ+oPiz8mIinuHbV6sxOLdvX1Jv/myk7XTK9orgt3tuEpBu/zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.1.tgz", + "integrity": "sha512-txEdZxPUgM1PwGvDvHzqhXisrc5LlRWYCf2yyHfvITWioAKat7srQvpjMAvgzf0t6t7j8yHrryXU9xt7RZqFpw==", "dependencies": { - "@smithy/types": "^2.8.0" + "@smithy/types": "^2.9.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.2.8.tgz", - "integrity": "sha512-E62byatbwSWrtq9RJ7xN40tqrRKDGrEL4EluyNpaIDvfvet06a/QC58oHw2FgVaEgkj0tXZPjZaKrhPfpoU0qw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.1.tgz", + "integrity": "sha512-2E2kh24igmIznHLB6H05Na4OgIEilRu0oQpYXo3LCNRrawHAcfDKq9004zJs+sAMt2X5AbY87CUCJ7IpqpSgdw==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7855,17 +8315,17 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.0.15.tgz", - "integrity": "sha512-SRTEJSEhQYVlBKIIdZ9SZpqW+KFqxqcNnEcBX+8xkDdWx+DItme9VcCDkdN32yTIrICC+irUufnUdV7mmHPjoA==", - "dependencies": { - "@smithy/eventstream-codec": "^2.0.13", - "@smithy/is-array-buffer": "^2.0.0", - "@smithy/types": "^2.5.0", - "@smithy/util-hex-encoding": "^2.0.0", - "@smithy/util-middleware": "^2.0.6", - "@smithy/util-uri-escape": "^2.0.0", - "@smithy/util-utf8": "^2.0.2", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.1.tgz", + "integrity": "sha512-Hb7xub0NHuvvQD3YwDSdanBmYukoEkhqBjqoxo+bSdC0ryV9cTfgmNjuAQhTPYB6yeU7hTR+sPRiFMlxqv6kmg==", + "dependencies": { + "@smithy/eventstream-codec": "^2.1.1", + "@smithy/is-array-buffer": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-uri-escape": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7873,15 +8333,15 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.2.1.tgz", - "integrity": "sha512-SpD7FLK92XV2fon2hMotaNDa2w5VAy5/uVjP9WFmjGSgWM8pTPVkHcDl1yFs5Z8LYbij0FSz+DbCBK6i+uXXUA==", - "dependencies": { - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", - "@smithy/util-stream": "^2.0.24", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.3.1.tgz", + "integrity": "sha512-YsTdU8xVD64r2pLEwmltrNvZV6XIAC50LN6ivDopdt+YiF/jGH6PY9zUOu0CXD/d8GMB8gbhnpPsdrjAXHS9QA==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-stream": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7889,9 +8349,9 @@ } }, "node_modules/@smithy/types": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.8.0.tgz", - "integrity": "sha512-h9sz24cFgt/W1Re22OlhQKmUZkNh244ApgRsUDYinqF8R+QgcsBIX344u2j61TPshsTz3CvL6HYU1DnQdsSrHA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.9.1.tgz", + "integrity": "sha512-vjXlKNXyprDYDuJ7UW5iobdmyDm6g8dDG+BFUncAg/3XJaN45Gy5RWWWUVgrzIK7S4R1KWgIX5LeJcfvSI24bw==", "dependencies": { "tslib": "^2.5.0" }, @@ -7900,21 +8360,21 @@ } }, "node_modules/@smithy/url-parser": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.16.tgz", - "integrity": "sha512-Wfz5WqAoRT91TjRy1JeLR0fXtkIXHGsMbgzKFTx7E68SrZ55TB8xoG+vm11Ru4gheFTMXjAjwAxv1jQdC+pAQA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.1.tgz", + "integrity": "sha512-qC9Bv8f/vvFIEkHsiNrUKYNl8uKQnn4BdhXl7VzQRP774AwIjiSMMwkbT+L7Fk8W8rzYVifzJNYxv1HwvfBo3Q==", "dependencies": { - "@smithy/querystring-parser": "^2.0.16", - "@smithy/types": "^2.8.0", + "@smithy/querystring-parser": "^2.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" } }, "node_modules/@smithy/util-base64": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.1.tgz", - "integrity": "sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.1.1.tgz", + "integrity": "sha512-UfHVpY7qfF/MrgndI5PexSKVTxSZIdz9InghTFa49QOvuu9I52zLPLUHXvHpNuMb1iD2vmc6R+zbv/bdMipR/g==", "dependencies": { - "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-buffer-from": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7922,17 +8382,17 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.1.tgz", - "integrity": "sha512-NXYp3ttgUlwkaug4bjBzJ5+yIbUbUx8VsSLuHZROQpoik+gRkIBeEG9MPVYfvPNpuXb/puqodeeUXcKFe7BLOQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.1.1.tgz", + "integrity": "sha512-ekOGBLvs1VS2d1zM2ER4JEeBWAvIOUKeaFch29UjjJsxmZ/f0L3K3x0dEETgh3Q9bkZNHgT+rkdl/J/VUqSRag==", "dependencies": { "tslib": "^2.5.0" } }, "node_modules/@smithy/util-body-length-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.1.0.tgz", - "integrity": "sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.2.1.tgz", + "integrity": "sha512-/ggJG+ta3IDtpNVq4ktmEUtOkH1LW64RHB5B0hcr5ZaWBmo96UX2cIOVbjCqqDickTXqBWZ4ZO0APuaPrD7Abg==", "dependencies": { "tslib": "^2.5.0" }, @@ -7941,11 +8401,11 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz", - "integrity": "sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz", + "integrity": "sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg==", "dependencies": { - "@smithy/is-array-buffer": "^2.0.0", + "@smithy/is-array-buffer": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -7953,9 +8413,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.1.0.tgz", - "integrity": "sha512-S6V0JvvhQgFSGLcJeT1CBsaTR03MM8qTuxMH9WPCCddlSo2W0V5jIHimHtIQALMLEDPGQ0ROSRr/dU0O+mxiQg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.2.1.tgz", + "integrity": "sha512-50VL/tx9oYYcjJn/qKqNy7sCtpD0+s8XEBamIFo4mFFTclKMNp+rsnymD796uybjiIquB7VCB/DeafduL0y2kw==", "dependencies": { "tslib": "^2.5.0" }, @@ -7964,13 +8424,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.24.tgz", - "integrity": "sha512-TsP5mBuLgO2C21+laNG2nHYZEyUdkbGURv2tHvSuQQxLz952MegX95uwdxOY2jR2H4GoKuVRfdJq7w4eIjGYeg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.1.tgz", + "integrity": "sha512-lqLz/9aWRO6mosnXkArtRuQqqZBhNpgI65YDpww4rVQBuUT7qzKbDLG5AmnQTCiU4rOquaZO/Kt0J7q9Uic7MA==", "dependencies": { - "@smithy/property-provider": "^2.0.17", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", "bowser": "^2.11.0", "tslib": "^2.5.0" }, @@ -7979,16 +8439,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.0.32", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.32.tgz", - "integrity": "sha512-d0S33dXA2cq1NyorVMroMrEtqKMr3MlyLITcfTBf9pXiigYiPMOtbSI7czHIfDbuVuM89Cg0urAgpt73QV9mPQ==", - "dependencies": { - "@smithy/config-resolver": "^2.0.23", - "@smithy/credential-provider-imds": "^2.1.5", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/property-provider": "^2.0.17", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.1.1.tgz", + "integrity": "sha512-tYVrc+w+jSBfBd267KDnvSGOh4NMz+wVH7v4CClDbkdPfnjvImBZsOURncT5jsFwR9KCuDyPoSZq4Pa6+eCUrA==", + "dependencies": { + "@smithy/config-resolver": "^2.1.1", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -7996,12 +8456,12 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.0.8.tgz", - "integrity": "sha512-l8zVuyZZ61IzZBYp5NWvsAhbaAjYkt0xg9R4xUASkg5SEeTT2meHOJwJHctKMFUXe4QZbn9fR2MaBYjP2119+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.1.tgz", + "integrity": "sha512-sI4d9rjoaekSGEtq3xSb2nMjHMx8QXcz2cexnVyRWsy4yQ9z3kbDpX+7fN0jnbdOp0b3KSTZJZ2Yb92JWSanLw==", "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/types": "^2.8.0", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -8009,9 +8469,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz", - "integrity": "sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz", + "integrity": "sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg==", "dependencies": { "tslib": "^2.5.0" }, @@ -8020,11 +8480,11 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.0.9.tgz", - "integrity": "sha512-PnCnBJ07noMX1lMDTEefmxSlusWJUiLfrme++MfK5TD0xz8NYmakgoXy5zkF/16zKGmiwOeKAztWT/Vjk1KRIQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.1.tgz", + "integrity": "sha512-mKNrk8oz5zqkNcbcgAAepeJbmfUW6ogrT2Z2gDbIUzVzNAHKJQTYmH9jcy0jbWb+m7ubrvXKb6uMjkSgAqqsFA==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -8032,12 +8492,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.0.9.tgz", - "integrity": "sha512-46BFWe9RqB6g7f4mxm3W3HlqknqQQmWHKlhoqSFZuGNuiDU5KqmpebMbvC3tjTlUkqn4xa2Z7s3Hwb0HNs5scw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.1.tgz", + "integrity": "sha512-Mg+xxWPTeSPrthpC5WAamJ6PW4Kbo01Fm7lWM1jmGRvmrRdsd3192Gz2fBXAMURyXpaNxyZf6Hr/nQ4q70oVEA==", "dependencies": { - "@smithy/service-error-classification": "^2.0.9", - "@smithy/types": "^2.8.0", + "@smithy/service-error-classification": "^2.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -8045,17 +8505,17 @@ } }, "node_modules/@smithy/util-stream": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.0.24.tgz", - "integrity": "sha512-hRpbcRrOxDriMVmbya+Mv77VZVupxRAsfxVDKS54XuiURhdiwCUXJP0X1iJhHinuUf6n8pBF0MkG9C8VooMnWw==", - "dependencies": { - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/types": "^2.8.0", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-buffer-from": "^2.0.0", - "@smithy/util-hex-encoding": "^2.0.0", - "@smithy/util-utf8": "^2.0.2", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.1.tgz", + "integrity": "sha512-J7SMIpUYvU4DQN55KmBtvaMc7NM3CZ2iWICdcgaovtLzseVhAqFRYqloT3mh0esrFw+3VEK6nQFteFsTqZSECQ==", + "dependencies": { + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -8063,9 +8523,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz", - "integrity": "sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.1.1.tgz", + "integrity": "sha512-saVzI1h6iRBUVSqtnlOnc9ssU09ypo7n+shdQ8hBTZno/9rZ3AuRYvoHInV57VF7Qn7B+pFJG7qTzFiHxWlWBw==", "dependencies": { "tslib": "^2.5.0" }, @@ -8074,11 +8534,11 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.2.tgz", - "integrity": "sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.1.1.tgz", + "integrity": "sha512-BqTpzYEcUMDwAKr7/mVRUtHDhs6ZoXDi9NypMvMfOr/+u1NW7JgqodPDECiiLboEm6bobcPcECxzjtQh865e9A==", "dependencies": { - "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-buffer-from": "^2.1.1", "tslib": "^2.5.0" }, "engines": { diff --git a/package.json b/package.json index 8ef9e5ef..1f3cc466 100644 --- a/package.json +++ b/package.json @@ -56,9 +56,10 @@ "@adobe/helix-status": "10.0.11", "@adobe/helix-universal-logger": "3.0.13", "@adobe/spacecat-shared-data-access": "1.9.1", - "@adobe/spacecat-shared-utils": "1.7.1", - "@adobe/spacecat-shared-rum-api-client": "1.3.2", "@adobe/spacecat-shared-http-utils": "1.1.1", + "@adobe/spacecat-shared-rum-api-client": "1.3.2", + "@adobe/spacecat-shared-utils": "1.7.1", + "@aws-sdk/client-cost-explorer": "3.499.0", "@aws-sdk/client-sqs": "3.490.0", "diff": "5.1.0", "urijs": "1.19.11" diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 30109213..1d6a62e1 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -9,11 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +import { CostExplorerClient, GetCostAndUsageCommand } from '@aws-sdk/client-cost-explorer'; +import { internalServerError, noContent } from '@adobe/spacecat-shared-http-utils'; -import AWSCostApiClient from '@adobe/spacecat-shared-aws-api-client'; - - - +/** + * This function calculates the previous month's start and end date based on current date + * and return the start and end date in YYYY/MM/DD format like 2021/01/01 and 2021/01/31 + * @returns {string,string} startDate + */ function calculatePreviousMonthDate() { const date = new Date(); let month = 1 + date.getMonth(); // as month starts from 0 @@ -22,48 +25,67 @@ function calculatePreviousMonthDate() { if (month === 1) { // if month is january then previous month will be december of previous year month = 12; year -= 1; - } - else { + } else { month -= 1; } const startDate = `${String(year).padStart(4, '0')}/${String(month).padStart(2, '0')}/01`; return { startDate, endDate }; } -const { startDate, endDate } = calculatePreviousMonthDate(); -const input = { - "TimePeriod": { - "End": endDate, - "Start": startDate - }, - "Granularity": "MONTHLY", - "Filter": { - "Tags": { - "Key": 'Adobe.ArchPath', - "Values": ['EC.SpaceCat.Services'], - "MatchOptions": ['EQUALS'] - } - }, - "Metrics": [ - "UnblendedCost" - ], - "GroupBy": [ - { - "Key": "SERVICE", - "Type": "DIMENSION" + +/** + * This function builds the input for AWS Cost Explorer API + * @param {string} start start date in YYYY/MM/DD format like 2021/01/01 + * @param {string} end end date in YYYY/MM/DD format like 2021/01/31 + * @returns {Object} input + */ +function buildAWSInput(start, end) { + const startDate = start || calculatePreviousMonthDate().startDate; + const endDate = end || calculatePreviousMonthDate().endDate; + return { + TimePeriod: { + End: endDate, + Start: startDate, }, - { - "Key": "Environment", - "Type": "TAG" - } - ] -}; + Granularity: 'MONTHLY', + Filter: { + Tags: { + Key: 'Adobe.ArchPath', + Values: ['EC.SpaceCat.Services'], + MatchOptions: ['EQUALS'], + }, + }, + Metrics: [ + 'UnblendedCost', + ], + GroupBy: [ + { + Key: 'SERVICE', + Type: 'DIMENSION', + }, + { + Key: 'Environment', + Type: 'TAG', + }, + ], + }; +} -const parseYearDate = (str) => { +/** + * This function parses the date in YYYY/MM/DD format like 2021/01/01 and return the date in + * @param {string} date format like 2021-01-01 + * @returns string like Jan-21 + */ +const parseYearDate = (date) => { const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - const date = new Date(str); - return `${months[date.getMonth()]}-${date.getFullYear().toString().substring(2)}`; + const monthYear = new Date(date); + return `${months[monthYear.getMonth()]}-${monthYear.getFullYear().toString().substring(2)}`; }; +/** + * This function maps the AWS service name to the service name used in SpaceCat + * @param {string} service AWS service name + * @returns string like LAMBDA + */ const serviceMap = (service) => { switch (service) { case 'AWS Lambda': @@ -83,55 +105,67 @@ const serviceMap = (service) => { } }; - /** - * url param in run-query@v3/rum-dashboard works in a 'startsWith' fashion. url=domain.com returns - * an empty result whereas url=www.domain.com/ returns the desired result. To catch the redirects - * to subdomains we issue a GET call to the domain, then use the final url after redirects - * @param url - * @returns finalUrl {Promise} + * This function processes the response from AWS Cost Explorer API + * @param {Object} data response from AWS Cost Explorer API + * @returns {Object} results by month and Year wise. */ - function processAWSResponse(data) { if (data && data.ResultsByTime && data.ResultsByTime.length > 0) { let result; - data.ResultsByTime.forEach((result) => { - if (result.Groups && result.Groups.length > 0) { + data.ResultsByTime.forEach((r) => { + if (r.Groups && r.Groups.length > 0) { let granularity; - result.Groups.forEach((group) => { + r.Groups.forEach((group) => { const key = group.Keys[0]; if (group.Metrics && group.Metrics.UnblendedCost) { + // eslint-disable-next-line max-len granularity[serviceMap(key)] = parseFloat(group.Metrics.UnblendedCost.Amount).toFixed(2); } }); - result[parseYearDate(result.TimePeriod.Start)] = granularity; + result[parseYearDate(r.TimePeriod.Start)] = granularity; } }); return result; } + return {}; } export default async function auditCOGs(message, context) { - const { type, url, auditContext } = message; + const { type, startDate, endDate } = message; const { log, sqs } = context; const { - AUDIT_RESULTS_QUEUE_URL: queueUrl, + AWS_ACCESS_KEY_ID: awsAccessKeyId, + AWS_SECRET_ACCESS_KEY: awsSecretAccessKey2, + AWS_REGION: awsRegion, } = context.env; try { - log.info(`Received audit req for domain: ${url}`); + log.info(`Fetching Cost Usage for ${awsRegion} from ${startDate} to ${endDate}`); - const awsAPIClient = AWSCostApiClient.createFrom(context); - - const data = await awsAPIClient.getUsageCost(input); - const auditResult = processAWSResponse(data); + const client = new CostExplorerClient({ + region: awsRegion, + credentials: { + accessKeyId: awsAccessKeyId, + secretAccessKey: awsSecretAccessKey2, + }, + }); + + const command = new GetCostAndUsageCommand(buildAWSInput(startDate, endDate)); + const response = await client.send(command); + const usageCost = processAWSResponse(response); + if (Object.keys(usageCost).length === 0) { + log.info(`No Cost Usage found for ${awsRegion} from ${startDate} to ${endDate}`); + return noContent(); + } - await sqs.sendMessage(queueUrl, { - type, - url, - auditContext, - auditResult, + usageCost.forEach(async (monthYear) => { + log.info(`Cost for ${monthYear} is ${usageCost[monthYear]}`); + await sqs.sendMessage(monthYear, { + type, + ...usageCost[monthYear], + }); }); - log.info(`Successfully audited ${url} for ${type} type audit`); + log.info(`Successfully fetched Cost Usage for ${awsRegion} from ${startDate} to ${endDate}`); return noContent(); } catch (e) { return internalServerError(`Internal server error: ${e.message}`); diff --git a/src/index.js b/src/index.js index d30dccf7..8d1afd02 100644 --- a/src/index.js +++ b/src/index.js @@ -21,12 +21,14 @@ import apex from './apex/handler.js'; import cwv from './cwv/handler.js'; import lhs from './lhs/handler.js'; import notfound from './notfound/handler.js'; +import cogs from './cogs/handler.js'; const HANDLERS = { apex, cwv, 'lhs-mobile': lhs, 'lhs-desktop': lhs, + cogs, 404: notfound, }; diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index e69de29b..179cfa47 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import wrap from '@adobe/helix-shared-wrap'; +import sinon from 'sinon'; +import chai from 'chai'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import nock from 'nock'; +import { main } from '../../src/index.js'; + +chai.use(sinonChai); +chai.use(chaiAsPromised); +const { expect } = chai; + +const sandbox = sinon.createSandbox(); + +describe('cogs handler test', () => { + let context; + beforeEach('setup', () => { + context = { + log: console, + runtime: { + region: 'us-east-1', + }, + env: { + AWS_ACCESS_KEY_ID: 'test', + AWS_SECRET_ACCESS_KEY: 'test', + AWS_REGION: 'us-east-1', + }, + }; + }); + afterEach('clean', () => { + sandbox.restore(); + }); +}); From 62ddc76a261b9ed66856ff2dd0d76f662d9a60e1 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Mon, 29 Jan 2024 18:57:21 -0800 Subject: [PATCH 03/17] AWS COGS Handler impementation --- src/cogs/handler.js | 11 +++-- test/audits/cogs.test.js | 88 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 1d6a62e1..637ff048 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -112,10 +112,10 @@ const serviceMap = (service) => { */ function processAWSResponse(data) { if (data && data.ResultsByTime && data.ResultsByTime.length > 0) { - let result; + const result = {}; data.ResultsByTime.forEach((r) => { if (r.Groups && r.Groups.length > 0) { - let granularity; + const granularity = {}; r.Groups.forEach((group) => { const key = group.Keys[0]; if (group.Metrics && group.Metrics.UnblendedCost) { @@ -148,8 +148,8 @@ export default async function auditCOGs(message, context) { secretAccessKey: awsSecretAccessKey2, }, }); - - const command = new GetCostAndUsageCommand(buildAWSInput(startDate, endDate)); + const input = buildAWSInput(startDate, endDate); + const command = new GetCostAndUsageCommand(input); const response = await client.send(command); const usageCost = processAWSResponse(response); if (Object.keys(usageCost).length === 0) { @@ -157,8 +157,7 @@ export default async function auditCOGs(message, context) { return noContent(); } - usageCost.forEach(async (monthYear) => { - log.info(`Cost for ${monthYear} is ${usageCost[monthYear]}`); + Object.keys(usageCost).forEach(async (monthYear) => { await sqs.sendMessage(monthYear, { type, ...usageCost[monthYear], diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index 179cfa47..228084f6 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -11,8 +11,6 @@ */ /* eslint-env mocha */ - -import wrap from '@adobe/helix-shared-wrap'; import sinon from 'sinon'; import chai from 'chai'; import sinonChai from 'sinon-chai'; @@ -28,7 +26,17 @@ const sandbox = sinon.createSandbox(); describe('cogs handler test', () => { let context; + const request = new Request('https://space.cat'); + let messageBodyJson; beforeEach('setup', () => { + messageBodyJson = { + type: 'cogs', + startDate: '2023-12-01', + endDate: '2024-01-01', + auditContext: { + finalUrl: 'adobe.com', + }, + }; context = { log: console, runtime: { @@ -37,11 +45,85 @@ describe('cogs handler test', () => { env: { AWS_ACCESS_KEY_ID: 'test', AWS_SECRET_ACCESS_KEY: 'test', - AWS_REGION: 'us-east-1', + AWS_REGION: 'us-west-2', + }, + invocation: { + event: { + Records: [{ + body: JSON.stringify(messageBodyJson), + }], + }, + }, + sqs: { + sendMessage: sandbox.stub().resolves(), }, }; }); afterEach('clean', () => { sandbox.restore(); }); + + it('cogs call failed on missing AWS_REGION', async () => { + delete context.env.AWS_REGION; + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(400, 'UnrecognizedClientException: unrecognized region'); + const result = await main(request, context); + expect(result.status).to.be.equal(500); + }); + it('reject when aws credentials are missing or wrong', async () => { + delete context.env.AWS_ACCESS_KEY_ID; + delete context.env.AWS_SECRET_ACCESS_KEY; + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(400, 'UnrecognizedClientException: The security token included in the request is invalid'); + const result = await main(request, context); + expect(result.status).to.be.equal(500); + }); + + it('reject when missing or wrong startDate', async () => { + delete messageBodyJson.startDate; + context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(400, 'ValidationException: Start time is invalid. Valid format is: yyyy-MM-dd.'); + const result = await main(request, context); + expect(result.status).to.be.equal(500); + }); + it('reject when missing or wrong endDate', async () => { + delete messageBodyJson.endDate; + context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(400, 'ValidationException: End time is invalid. Valid format is: yyyy-MM-dd.'); + const result = await main(request, context); + expect(result.status).to.be.equal(500); + }); + it('check for starting date of January', async () => { + messageBodyJson.startDate = '2024-01-01'; + messageBodyJson.endDate = '2024-02-01'; + context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(200, 'OK'); + await expect(main(request, context)).to.be.fulfilled; + }); + it('check for starting date of December', async () => { + messageBodyJson.startDate = '2023-12-01'; + messageBodyJson.endDate = '2024-01-01'; + context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(200, 'OK'); + await expect(main(request, context)).to.be.fulfilled; + }); + it('should pass on trigger cogs audit', async () => { + messageBodyJson.startDate = '2023-12-01'; + messageBodyJson.endDate = '2024-01-01'; + context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(200, 'OK'); + await expect(main(request, context)).to.be.fulfilled; + }); }); From 03c6a67c73c8de091105fa1c07298a53733c5faf Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Mon, 29 Jan 2024 23:05:57 -0800 Subject: [PATCH 04/17] Updated logging --- src/cogs/handler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 637ff048..4dc2025e 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -167,6 +167,7 @@ export default async function auditCOGs(message, context) { log.info(`Successfully fetched Cost Usage for ${awsRegion} from ${startDate} to ${endDate}`); return noContent(); } catch (e) { - return internalServerError(`Internal server error: ${e.message}`); + log.error(JSON.stringify(e)); + return internalServerError('Internal server error'); } } From 6055ee05b46dee579a3405747e1b4a2a3bc48be4 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Tue, 30 Jan 2024 09:22:17 -0800 Subject: [PATCH 05/17] removed AWS credential requirements --- src/cogs/handler.js | 38 +++++++++++------------------- test/audits/cogs.test.js | 51 +++++----------------------------------- 2 files changed, 20 insertions(+), 69 deletions(-) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 4dc2025e..717f531b 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -133,30 +133,20 @@ function processAWSResponse(data) { export default async function auditCOGs(message, context) { const { type, startDate, endDate } = message; const { log, sqs } = context; - const { - AWS_ACCESS_KEY_ID: awsAccessKeyId, - AWS_SECRET_ACCESS_KEY: awsSecretAccessKey2, - AWS_REGION: awsRegion, - } = context.env; - try { - log.info(`Fetching Cost Usage for ${awsRegion} from ${startDate} to ${endDate}`); - - const client = new CostExplorerClient({ - region: awsRegion, - credentials: { - accessKeyId: awsAccessKeyId, - secretAccessKey: awsSecretAccessKey2, - }, - }); - const input = buildAWSInput(startDate, endDate); - const command = new GetCostAndUsageCommand(input); - const response = await client.send(command); - const usageCost = processAWSResponse(response); - if (Object.keys(usageCost).length === 0) { - log.info(`No Cost Usage found for ${awsRegion} from ${startDate} to ${endDate}`); - return noContent(); - } + log.info(`Fetching Cost Usage from ${startDate} to ${endDate}`); + const client = new CostExplorerClient(); + const input = buildAWSInput(startDate, endDate); + const command = new GetCostAndUsageCommand(input); + const response = await client.send(command); + log.info(JSON.stringify(response)); + log.info('testing'); + const usageCost = processAWSResponse(response); + if (Object.keys(usageCost).length === 0) { + log.info(`No Cost Usage found from ${startDate} to ${endDate}`); + return noContent(); + } + try { Object.keys(usageCost).forEach(async (monthYear) => { await sqs.sendMessage(monthYear, { type, @@ -164,7 +154,7 @@ export default async function auditCOGs(message, context) { }); }); - log.info(`Successfully fetched Cost Usage for ${awsRegion} from ${startDate} to ${endDate}`); + log.info(`Successfully fetched Cost Usage from ${startDate} to ${endDate}`); return noContent(); } catch (e) { log.error(JSON.stringify(e)); diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index 228084f6..f8db8118 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -16,7 +16,7 @@ import chai from 'chai'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; import nock from 'nock'; -import { main } from '../../src/index.js'; +import main from '../../src/cogs/handler.js'; chai.use(sinonChai); chai.use(chaiAsPromised); @@ -26,34 +26,18 @@ const sandbox = sinon.createSandbox(); describe('cogs handler test', () => { let context; - const request = new Request('https://space.cat'); let messageBodyJson; beforeEach('setup', () => { messageBodyJson = { type: 'cogs', startDate: '2023-12-01', endDate: '2024-01-01', - auditContext: { - finalUrl: 'adobe.com', - }, }; context = { log: console, runtime: { region: 'us-east-1', }, - env: { - AWS_ACCESS_KEY_ID: 'test', - AWS_SECRET_ACCESS_KEY: 'test', - AWS_REGION: 'us-west-2', - }, - invocation: { - event: { - Records: [{ - body: JSON.stringify(messageBodyJson), - }], - }, - }, sqs: { sendMessage: sandbox.stub().resolves(), }, @@ -63,67 +47,44 @@ describe('cogs handler test', () => { sandbox.restore(); }); - it('cogs call failed on missing AWS_REGION', async () => { - delete context.env.AWS_REGION; - nock('https://ce.us-east-1.amazonaws.com') - .post('/') - .reply(400, 'UnrecognizedClientException: unrecognized region'); - const result = await main(request, context); - expect(result.status).to.be.equal(500); - }); - it('reject when aws credentials are missing or wrong', async () => { - delete context.env.AWS_ACCESS_KEY_ID; - delete context.env.AWS_SECRET_ACCESS_KEY; - nock('https://ce.us-east-1.amazonaws.com') - .post('/') - .reply(400, 'UnrecognizedClientException: The security token included in the request is invalid'); - const result = await main(request, context); - expect(result.status).to.be.equal(500); - }); - it('reject when missing or wrong startDate', async () => { delete messageBodyJson.startDate; - context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); nock('https://ce.us-east-1.amazonaws.com') .post('/') .reply(400, 'ValidationException: Start time is invalid. Valid format is: yyyy-MM-dd.'); - const result = await main(request, context); + const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(500); }); it('reject when missing or wrong endDate', async () => { delete messageBodyJson.endDate; - context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); nock('https://ce.us-east-1.amazonaws.com') .post('/') .reply(400, 'ValidationException: End time is invalid. Valid format is: yyyy-MM-dd.'); - const result = await main(request, context); + const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(500); }); it('check for starting date of January', async () => { messageBodyJson.startDate = '2024-01-01'; messageBodyJson.endDate = '2024-02-01'; - context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); nock('https://ce.us-east-1.amazonaws.com') .post('/') .reply(200, 'OK'); - await expect(main(request, context)).to.be.fulfilled; + await expect(main(messageBodyJson, context)).to.be.fulfilled; }); it('check for starting date of December', async () => { messageBodyJson.startDate = '2023-12-01'; messageBodyJson.endDate = '2024-01-01'; - context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); nock('https://ce.us-east-1.amazonaws.com') .post('/') .reply(200, 'OK'); - await expect(main(request, context)).to.be.fulfilled; + await expect(main(messageBodyJson, context)).to.be.fulfilled; }); it('should pass on trigger cogs audit', async () => { messageBodyJson.startDate = '2023-12-01'; messageBodyJson.endDate = '2024-01-01'; - context.invocation.event.Records[0].body = JSON.stringify(messageBodyJson); nock('https://ce.us-east-1.amazonaws.com') .post('/') .reply(200, 'OK'); - await expect(main(request, context)).to.be.fulfilled; + await expect(main(messageBodyJson, context)).to.be.fulfilled; }); }); From befa0d5e183cc1ccfa7f9bf04d71bbcb5b0940e5 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Tue, 30 Jan 2024 19:09:02 -0800 Subject: [PATCH 06/17] fixing test cases --- src/cogs/handler.js | 55 +++++++--------------- test/audits/cogs.json | 99 ++++++++++++++++++++++++++++++++++++++++ test/audits/cogs.test.js | 59 +++++++++++++++++++++--- 3 files changed, 167 insertions(+), 46 deletions(-) create mode 100644 test/audits/cogs.json diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 717f531b..578c0116 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -10,37 +10,15 @@ * governing permissions and limitations under the License. */ import { CostExplorerClient, GetCostAndUsageCommand } from '@aws-sdk/client-cost-explorer'; -import { internalServerError, noContent } from '@adobe/spacecat-shared-http-utils'; - -/** - * This function calculates the previous month's start and end date based on current date - * and return the start and end date in YYYY/MM/DD format like 2021/01/01 and 2021/01/31 - * @returns {string,string} startDate - */ -function calculatePreviousMonthDate() { - const date = new Date(); - let month = 1 + date.getMonth(); // as month starts from 0 - let year = date.getFullYear(); - const endDate = `${String(year).padStart(4, '0')}/${String(month).padStart(2, '0')}/01`; - if (month === 1) { // if month is january then previous month will be december of previous year - month = 12; - year -= 1; - } else { - month -= 1; - } - const startDate = `${String(year).padStart(4, '0')}/${String(month).padStart(2, '0')}/01`; - return { startDate, endDate }; -} +import { internalServerError, noContent, notFound } from '@adobe/spacecat-shared-http-utils'; /** * This function builds the input for AWS Cost Explorer API - * @param {string} start start date in YYYY/MM/DD format like 2021/01/01 - * @param {string} end end date in YYYY/MM/DD format like 2021/01/31 + * @param {string} startDate start date in YYYY/MM/DD format like 2021/01/01 + * @param {string} endDate end date in YYYY/MM/DD format like 2021/01/31 * @returns {Object} input */ -function buildAWSInput(start, end) { - const startDate = start || calculatePreviousMonthDate().startDate; - const endDate = end || calculatePreviousMonthDate().endDate; +function buildAWSInput(startDate, endDate) { return { TimePeriod: { End: endDate, @@ -133,20 +111,20 @@ function processAWSResponse(data) { export default async function auditCOGs(message, context) { const { type, startDate, endDate } = message; const { log, sqs } = context; + const { awsRegion } = context.runtime; + log.info(`Fetching Cost Usage from ${startDate} to ${endDate}`); - const client = new CostExplorerClient(); - const input = buildAWSInput(startDate, endDate); - const command = new GetCostAndUsageCommand(input); - const response = await client.send(command); - log.info(JSON.stringify(response)); - log.info('testing'); - const usageCost = processAWSResponse(response); - if (Object.keys(usageCost).length === 0) { - log.info(`No Cost Usage found from ${startDate} to ${endDate}`); - return noContent(); - } try { + const client = new CostExplorerClient({ region: awsRegion }); + const input = buildAWSInput(startDate, endDate); + const command = new GetCostAndUsageCommand(input); + const response = await client.send(command); + const usageCost = processAWSResponse(response); + if (Object.keys(usageCost).length === 0) { + log.info(`No Cost Usage found from ${startDate} to ${endDate}`); + return notFound('No Cost Usage found'); + } Object.keys(usageCost).forEach(async (monthYear) => { await sqs.sendMessage(monthYear, { type, @@ -157,7 +135,6 @@ export default async function auditCOGs(message, context) { log.info(`Successfully fetched Cost Usage from ${startDate} to ${endDate}`); return noContent(); } catch (e) { - log.error(JSON.stringify(e)); - return internalServerError('Internal server error'); + return internalServerError(e.message); } } diff --git a/test/audits/cogs.json b/test/audits/cogs.json new file mode 100644 index 00000000..dbb9ec7d --- /dev/null +++ b/test/audits/cogs.json @@ -0,0 +1,99 @@ +{ + "DimensionValueAttributes": [], + "GroupDefinitions": [ + { + "Key": "SERVICE", + "Type": "DIMENSION" + }, + { + "Key": "Environment", + "Type": "TAG" + } + ], + "NextPageToken": null, + "ResultsByTime": [ + { + "Estimated": false, + "Groups": [ + { + "Keys": [ + "AWS Lambda", + "Environment$" + ], + "Metrics": { + "UnblendedCost": { + "Amount": "14.6732180628", + "Unit": "USD" + } + } + }, + { + "Keys": [ + "AWS Secrets Manager", + "Environment$" + ], + "Metrics": { + "UnblendedCost": { + "Amount": "0.093415", + "Unit": "USD" + } + } + }, + { + "Keys": [ + "Amazon DynamoDB", + "Environment$" + ], + "Metrics": { + "UnblendedCost": { + "Amount": "0.095706623", + "Unit": "USD" + } + } + }, + { + "Keys": [ + "Amazon Simple Queue Service", + "Environment$" + ], + "Metrics": { + "UnblendedCost": { + "Amount": "0.17150958", + "Unit": "USD" + } + } + }, + { + "Keys": [ + "Amazon Simple Storage Service", + "Environment$" + ], + "Metrics": { + "UnblendedCost": { + "Amount": "0.0007143178", + "Unit": "USD" + } + } + }, + { + "Keys": [ + "AmazonCloudWatch", + "Environment$" + ], + "Metrics": { + "UnblendedCost": { + "Amount": "0.049616647", + "Unit": "USD" + } + } + } + ], + "TimePeriod": { + "End": "2024-01-01", + "Start": "2023-12-01" + }, + "Total": {} + } + ], + "ResultMetadata": {} + } \ No newline at end of file diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index f8db8118..f805a57a 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -16,8 +16,12 @@ import chai from 'chai'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; import nock from 'nock'; +import { createRequire } from 'module'; import main from '../../src/cogs/handler.js'; +const require = createRequire(import.meta.url); +const cogsResponse = require('./cogs.json'); + chai.use(sinonChai); chai.use(chaiAsPromised); const { expect } = chai; @@ -47,19 +51,28 @@ describe('cogs handler test', () => { sandbox.restore(); }); - it('reject when missing or wrong startDate', async () => { + it('raise error, when missing startDate input', async () => { delete messageBodyJson.startDate; nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(400, 'ValidationException: Start time is invalid. Valid format is: yyyy-MM-dd.'); + .reply(400, { message: 'Start time is invalid. Valid format is: yyyy-MM-dd' }); const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(500); }); - it('reject when missing or wrong endDate', async () => { + it('Calulate endDate, when missing in input', async () => { delete messageBodyJson.endDate; nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(400, 'ValidationException: End time is invalid. Valid format is: yyyy-MM-dd.'); + .reply(400, { message: 'End time is invalid. Valid format is: yyyy-MM-dd' }); + const result = await main(messageBodyJson, context); + expect(result.status).to.be.equal(500); + }); + it('Calulate startDate and endDate, when both missing in input', async () => { + delete messageBodyJson.startDate; + delete messageBodyJson.endDate; + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(400, { message: 'Time is invalid. Valid format is: yyyy-MM-dd' }); const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(500); }); @@ -68,7 +81,7 @@ describe('cogs handler test', () => { messageBodyJson.endDate = '2024-02-01'; nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(200, 'OK'); + .reply(200, cogsResponse); await expect(main(messageBodyJson, context)).to.be.fulfilled; }); it('check for starting date of December', async () => { @@ -76,7 +89,7 @@ describe('cogs handler test', () => { messageBodyJson.endDate = '2024-01-01'; nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(200, 'OK'); + .reply(200, cogsResponse); await expect(main(messageBodyJson, context)).to.be.fulfilled; }); it('should pass on trigger cogs audit', async () => { @@ -84,7 +97,39 @@ describe('cogs handler test', () => { messageBodyJson.endDate = '2024-01-01'; nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(200, 'OK'); + .reply(200, cogsResponse); + await expect(main(messageBodyJson, context)).to.be.fulfilled; + }); + it('test for new service data', async () => { + cogsResponse.ResultsByTime[0].Groups.push({ + Keys: [ + 'AmazonNewService', + 'Environment$', + ], + Metrics: { + UnblendedCost: { + Amount: '0.049616647', + Unit: 'USD', + }, + }, + }); + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(200, cogsResponse); + await expect(main(messageBodyJson, context)).to.be.fulfilled; + }); + it('test for empty group data set', async () => { + cogsResponse.ResultsByTime[0].Groups = []; + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(200, cogsResponse); + await expect(main(messageBodyJson, context)).to.be.fulfilled; + }); + it('test for empty data set', async () => { + cogsResponse.ResultsByTime = []; + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(200, cogsResponse); await expect(main(messageBodyJson, context)).to.be.fulfilled; }); }); From c927aac39aaaa0e44a7263845d39dbe1e5b461ac Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Tue, 30 Jan 2024 19:23:49 -0800 Subject: [PATCH 07/17] fixing code coverage test cases --- test/audits/cogs.test.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index f805a57a..2a23692f 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -59,7 +59,7 @@ describe('cogs handler test', () => { const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(500); }); - it('Calulate endDate, when missing in input', async () => { + it('raise error, , when missing endDate input', async () => { delete messageBodyJson.endDate; nock('https://ce.us-east-1.amazonaws.com') .post('/') @@ -67,7 +67,7 @@ describe('cogs handler test', () => { const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(500); }); - it('Calulate startDate and endDate, when both missing in input', async () => { + it('raise error, when both startDate and endDate are missing in input', async () => { delete messageBodyJson.startDate; delete messageBodyJson.endDate; nock('https://ce.us-east-1.amazonaws.com') @@ -76,23 +76,7 @@ describe('cogs handler test', () => { const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(500); }); - it('check for starting date of January', async () => { - messageBodyJson.startDate = '2024-01-01'; - messageBodyJson.endDate = '2024-02-01'; - nock('https://ce.us-east-1.amazonaws.com') - .post('/') - .reply(200, cogsResponse); - await expect(main(messageBodyJson, context)).to.be.fulfilled; - }); - it('check for starting date of December', async () => { - messageBodyJson.startDate = '2023-12-01'; - messageBodyJson.endDate = '2024-01-01'; - nock('https://ce.us-east-1.amazonaws.com') - .post('/') - .reply(200, cogsResponse); - await expect(main(messageBodyJson, context)).to.be.fulfilled; - }); - it('should pass on trigger cogs audit', async () => { + it('should pass on correct inputs', async () => { messageBodyJson.startDate = '2023-12-01'; messageBodyJson.endDate = '2024-01-01'; nock('https://ce.us-east-1.amazonaws.com') From 48a9cf63a9e9840e6c636c5fbf9cd09a39a3cdb1 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Tue, 30 Jan 2024 21:37:09 -0800 Subject: [PATCH 08/17] it test update --- test/audits/cogs.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index 2a23692f..e06fdd73 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -19,9 +19,6 @@ import nock from 'nock'; import { createRequire } from 'module'; import main from '../../src/cogs/handler.js'; -const require = createRequire(import.meta.url); -const cogsResponse = require('./cogs.json'); - chai.use(sinonChai); chai.use(chaiAsPromised); const { expect } = chai; @@ -31,7 +28,10 @@ const sandbox = sinon.createSandbox(); describe('cogs handler test', () => { let context; let messageBodyJson; + let cogsResponse; beforeEach('setup', () => { + const require = createRequire(import.meta.url); + cogsResponse = require('./cogs.json'); messageBodyJson = { type: 'cogs', startDate: '2023-12-01', From d9d08a1fed1b7263c822ae8698f5d4e5f7277636 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Tue, 30 Jan 2024 21:44:50 -0800 Subject: [PATCH 09/17] it test update --- src/cogs/handler.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 578c0116..21b8ff71 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -120,6 +120,7 @@ export default async function auditCOGs(message, context) { const input = buildAWSInput(startDate, endDate); const command = new GetCostAndUsageCommand(input); const response = await client.send(command); + log.info('AWS Cost Explorer API response', JSON.stringify(response)); const usageCost = processAWSResponse(response); if (Object.keys(usageCost).length === 0) { log.info(`No Cost Usage found from ${startDate} to ${endDate}`); From 5b2834d2fca02fcdd213b4fb43839963f051b75d Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Tue, 30 Jan 2024 21:47:05 -0800 Subject: [PATCH 10/17] it test update --- src/cogs/handler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 21b8ff71..578c0116 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -120,7 +120,6 @@ export default async function auditCOGs(message, context) { const input = buildAWSInput(startDate, endDate); const command = new GetCostAndUsageCommand(input); const response = await client.send(command); - log.info('AWS Cost Explorer API response', JSON.stringify(response)); const usageCost = processAWSResponse(response); if (Object.keys(usageCost).length === 0) { log.info(`No Cost Usage found from ${startDate} to ${endDate}`); From 81064ac2a6eff3210b6c9e22e186dcc1e4c6086e Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Tue, 30 Jan 2024 23:10:07 -0800 Subject: [PATCH 11/17] COGS API use the specific credentials to access the data --- src/cogs/handler.js | 14 +++++++++++++- test/audits/cogs.test.js | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 578c0116..a07ed656 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -112,11 +112,23 @@ export default async function auditCOGs(message, context) { const { type, startDate, endDate } = message; const { log, sqs } = context; const { awsRegion } = context.runtime; + const { + COGS_AWS_ACCESS_KEY: cogsAwsAccessKey, + COGS_AWS_SECRET_ACCESS_KEY: cogsAwsSecretKey, + } = context.env; log.info(`Fetching Cost Usage from ${startDate} to ${endDate}`); try { - const client = new CostExplorerClient({ region: awsRegion }); + const client = new CostExplorerClient( + { + region: awsRegion, + credentials: { + accessKeyId: cogsAwsAccessKey, + secretAccessKey: cogsAwsSecretKey, + }, + }, + ); const input = buildAWSInput(startDate, endDate); const command = new GetCostAndUsageCommand(input); const response = await client.send(command); diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index e06fdd73..6bc3c121 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -45,6 +45,10 @@ describe('cogs handler test', () => { sqs: { sendMessage: sandbox.stub().resolves(), }, + env: { + COGS_AWS_ACCESS_KEY: 'test', + COGS_AWS_SECRET_ACCESS_KEY: 'test', + }, }; }); afterEach('clean', () => { From cb155eec14aba5d39ffa68c982a8f49b28481b6b Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Wed, 31 Jan 2024 09:25:19 -0800 Subject: [PATCH 12/17] Removed AWS Credentials from codebase --- src/cogs/handler.js | 15 +-------------- test/audits/cogs.test.js | 4 ---- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index a07ed656..7be420b7 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -112,23 +112,10 @@ export default async function auditCOGs(message, context) { const { type, startDate, endDate } = message; const { log, sqs } = context; const { awsRegion } = context.runtime; - const { - COGS_AWS_ACCESS_KEY: cogsAwsAccessKey, - COGS_AWS_SECRET_ACCESS_KEY: cogsAwsSecretKey, - } = context.env; - log.info(`Fetching Cost Usage from ${startDate} to ${endDate}`); try { - const client = new CostExplorerClient( - { - region: awsRegion, - credentials: { - accessKeyId: cogsAwsAccessKey, - secretAccessKey: cogsAwsSecretKey, - }, - }, - ); + const client = new CostExplorerClient({ region: awsRegion }); const input = buildAWSInput(startDate, endDate); const command = new GetCostAndUsageCommand(input); const response = await client.send(command); diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index 6bc3c121..e06fdd73 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -45,10 +45,6 @@ describe('cogs handler test', () => { sqs: { sendMessage: sandbox.stub().resolves(), }, - env: { - COGS_AWS_ACCESS_KEY: 'test', - COGS_AWS_SECRET_ACCESS_KEY: 'test', - }, }; }); afterEach('clean', () => { From af330c39844269b4923485e41945759c6945ca32 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Wed, 31 Jan 2024 12:57:48 -0800 Subject: [PATCH 13/17] Fixed test cases --- src/cogs/handler.js | 9 ++- test/audits/cogs.json | 99 ------------------------------ test/audits/cogs.test.js | 35 ++++++----- test/fixtures/cogs-data.js | 120 +++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 119 deletions(-) delete mode 100644 test/audits/cogs.json create mode 100644 test/fixtures/cogs-data.js diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 7be420b7..acf8077a 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -55,8 +55,8 @@ function buildAWSInput(startDate, endDate) { */ const parseYearDate = (date) => { const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - const monthYear = new Date(date); - return `${months[monthYear.getMonth()]}-${monthYear.getFullYear().toString().substring(2)}`; + const yrMonth = date.split(/-/); + return `${months[yrMonth[1] - 1]}-${yrMonth[0].substring(2)}`; }; /** @@ -111,17 +111,16 @@ function processAWSResponse(data) { export default async function auditCOGs(message, context) { const { type, startDate, endDate } = message; const { log, sqs } = context; - const { awsRegion } = context.runtime; + const { region } = context.runtime; log.info(`Fetching Cost Usage from ${startDate} to ${endDate}`); try { - const client = new CostExplorerClient({ region: awsRegion }); + const client = new CostExplorerClient({ region }); const input = buildAWSInput(startDate, endDate); const command = new GetCostAndUsageCommand(input); const response = await client.send(command); const usageCost = processAWSResponse(response); if (Object.keys(usageCost).length === 0) { - log.info(`No Cost Usage found from ${startDate} to ${endDate}`); return notFound('No Cost Usage found'); } Object.keys(usageCost).forEach(async (monthYear) => { diff --git a/test/audits/cogs.json b/test/audits/cogs.json deleted file mode 100644 index dbb9ec7d..00000000 --- a/test/audits/cogs.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "DimensionValueAttributes": [], - "GroupDefinitions": [ - { - "Key": "SERVICE", - "Type": "DIMENSION" - }, - { - "Key": "Environment", - "Type": "TAG" - } - ], - "NextPageToken": null, - "ResultsByTime": [ - { - "Estimated": false, - "Groups": [ - { - "Keys": [ - "AWS Lambda", - "Environment$" - ], - "Metrics": { - "UnblendedCost": { - "Amount": "14.6732180628", - "Unit": "USD" - } - } - }, - { - "Keys": [ - "AWS Secrets Manager", - "Environment$" - ], - "Metrics": { - "UnblendedCost": { - "Amount": "0.093415", - "Unit": "USD" - } - } - }, - { - "Keys": [ - "Amazon DynamoDB", - "Environment$" - ], - "Metrics": { - "UnblendedCost": { - "Amount": "0.095706623", - "Unit": "USD" - } - } - }, - { - "Keys": [ - "Amazon Simple Queue Service", - "Environment$" - ], - "Metrics": { - "UnblendedCost": { - "Amount": "0.17150958", - "Unit": "USD" - } - } - }, - { - "Keys": [ - "Amazon Simple Storage Service", - "Environment$" - ], - "Metrics": { - "UnblendedCost": { - "Amount": "0.0007143178", - "Unit": "USD" - } - } - }, - { - "Keys": [ - "AmazonCloudWatch", - "Environment$" - ], - "Metrics": { - "UnblendedCost": { - "Amount": "0.049616647", - "Unit": "USD" - } - } - } - ], - "TimePeriod": { - "End": "2024-01-01", - "Start": "2023-12-01" - }, - "Total": {} - } - ], - "ResultMetadata": {} - } \ No newline at end of file diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index e06fdd73..5c3ce949 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -16,8 +16,8 @@ import chai from 'chai'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; import nock from 'nock'; -import { createRequire } from 'module'; import main from '../../src/cogs/handler.js'; +import { expectedCogsResult, expectedCOGSValue } from '../fixtures/cogs-data.js'; chai.use(sinonChai); chai.use(chaiAsPromised); @@ -28,10 +28,8 @@ const sandbox = sinon.createSandbox(); describe('cogs handler test', () => { let context; let messageBodyJson; - let cogsResponse; + const COGS_RESULT_MONTH_YEAR = 'Dec-23'; beforeEach('setup', () => { - const require = createRequire(import.meta.url); - cogsResponse = require('./cogs.json'); messageBodyJson = { type: 'cogs', startDate: '2023-12-01', @@ -81,11 +79,15 @@ describe('cogs handler test', () => { messageBodyJson.endDate = '2024-01-01'; nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(200, cogsResponse); - await expect(main(messageBodyJson, context)).to.be.fulfilled; + .reply(200, expectedCogsResult); + const result = await main(messageBodyJson, context); + expect(result.status).to.be.equal(204); + expect(context.sqs.sendMessage).to.have.been.calledOnce; + expect(context.sqs.sendMessage).to.have.been + .calledWith(COGS_RESULT_MONTH_YEAR, expectedCOGSValue); }); it('test for new service data', async () => { - cogsResponse.ResultsByTime[0].Groups.push({ + expectedCogsResult.ResultsByTime[0].Groups.push({ Keys: [ 'AmazonNewService', 'Environment$', @@ -99,21 +101,24 @@ describe('cogs handler test', () => { }); nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(200, cogsResponse); - await expect(main(messageBodyJson, context)).to.be.fulfilled; + .reply(200, expectedCogsResult); + const result = await main(messageBodyJson, context); + expect(result.status).to.be.equal(204); }); it('test for empty group data set', async () => { - cogsResponse.ResultsByTime[0].Groups = []; + expectedCogsResult.ResultsByTime[0].Groups = []; nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(200, cogsResponse); - await expect(main(messageBodyJson, context)).to.be.fulfilled; + .reply(200, expectedCogsResult); + const result = await main(messageBodyJson, context); + expect(result.status).to.be.equal(404); }); it('test for empty data set', async () => { - cogsResponse.ResultsByTime = []; + expectedCogsResult.ResultsByTime = []; nock('https://ce.us-east-1.amazonaws.com') .post('/') - .reply(200, cogsResponse); - await expect(main(messageBodyJson, context)).to.be.fulfilled; + .reply(200, expectedCogsResult); + const result = await main(messageBodyJson, context); + expect(result.status).to.be.equal(404); }); }); diff --git a/test/fixtures/cogs-data.js b/test/fixtures/cogs-data.js new file mode 100644 index 00000000..ddf2d8c5 --- /dev/null +++ b/test/fixtures/cogs-data.js @@ -0,0 +1,120 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export const expectedCogsResult = { + DimensionValueAttributes: [], + GroupDefinitions: [ + { + Key: 'SERVICE', + Type: 'DIMENSION', + }, + { + Key: 'Environment', + Type: 'TAG', + }, + ], + NextPageToken: null, + ResultsByTime: [ + { + Estimated: false, + Groups: [ + { + Keys: [ + 'AWS Lambda', + 'Environment$', + ], + Metrics: { + UnblendedCost: { + Amount: '14.6732180628', + Unit: 'USD', + }, + }, + }, + { + Keys: [ + 'AWS Secrets Manager', + 'Environment$', + ], + Metrics: { + UnblendedCost: { + Amount: '0.093415', + Unit: 'USD', + }, + }, + }, + { + Keys: [ + 'Amazon DynamoDB', + 'Environment$', + ], + Metrics: { + UnblendedCost: { + Amount: '0.095706623', + Unit: 'USD', + }, + }, + }, + { + Keys: [ + 'Amazon Simple Queue Service', + 'Environment$', + ], + Metrics: { + UnblendedCost: { + Amount: '0.17150958', + Unit: 'USD', + }, + }, + }, + { + Keys: [ + 'Amazon Simple Storage Service', + 'Environment$', + ], + Metrics: { + UnblendedCost: { + Amount: '0.0007143178', + Unit: 'USD', + }, + }, + }, + { + Keys: [ + 'AmazonCloudWatch', + 'Environment$', + ], + Metrics: { + UnblendedCost: { + Amount: '0.049616647', + Unit: 'USD', + }, + }, + }, + ], + TimePeriod: { + End: '2024-01-01', + Start: '2023-12-01', + }, + Total: {}, + }, + ], + ResultMetadata: {}, +}; +export const expectedCOGSValue = { + type: 'cogs', + LAMBDA: '14.67', + 'SECRETS MANAGER': '0.09', + DYNAMODB: '0.10', + SQS: '0.17', + S3: '0.00', + CLOUDWATCH: '0.05', +}; From 4ab7671c7e29c345c52c282c146cd1dbadf70b25 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Wed, 31 Jan 2024 13:36:34 -0800 Subject: [PATCH 14/17] Updated test case names --- test/audits/cogs.test.js | 60 +++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index 5c3ce949..e9ee9bb7 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -25,7 +25,7 @@ const { expect } = chai; const sandbox = sinon.createSandbox(); -describe('cogs handler test', () => { +describe('COGS handler test', () => { let context; let messageBodyJson; const COGS_RESULT_MONTH_YEAR = 'Dec-23'; @@ -49,34 +49,7 @@ describe('cogs handler test', () => { sandbox.restore(); }); - it('raise error, when missing startDate input', async () => { - delete messageBodyJson.startDate; - nock('https://ce.us-east-1.amazonaws.com') - .post('/') - .reply(400, { message: 'Start time is invalid. Valid format is: yyyy-MM-dd' }); - const result = await main(messageBodyJson, context); - expect(result.status).to.be.equal(500); - }); - it('raise error, , when missing endDate input', async () => { - delete messageBodyJson.endDate; - nock('https://ce.us-east-1.amazonaws.com') - .post('/') - .reply(400, { message: 'End time is invalid. Valid format is: yyyy-MM-dd' }); - const result = await main(messageBodyJson, context); - expect(result.status).to.be.equal(500); - }); - it('raise error, when both startDate and endDate are missing in input', async () => { - delete messageBodyJson.startDate; - delete messageBodyJson.endDate; - nock('https://ce.us-east-1.amazonaws.com') - .post('/') - .reply(400, { message: 'Time is invalid. Valid format is: yyyy-MM-dd' }); - const result = await main(messageBodyJson, context); - expect(result.status).to.be.equal(500); - }); it('should pass on correct inputs', async () => { - messageBodyJson.startDate = '2023-12-01'; - messageBodyJson.endDate = '2024-01-01'; nock('https://ce.us-east-1.amazonaws.com') .post('/') .reply(200, expectedCogsResult); @@ -86,7 +59,7 @@ describe('cogs handler test', () => { expect(context.sqs.sendMessage).to.have.been .calledWith(COGS_RESULT_MONTH_YEAR, expectedCOGSValue); }); - it('test for new service data', async () => { + it('Should test for new service data', async () => { expectedCogsResult.ResultsByTime[0].Groups.push({ Keys: [ 'AmazonNewService', @@ -105,7 +78,7 @@ describe('cogs handler test', () => { const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(204); }); - it('test for empty group data set', async () => { + it('Should test for empty group data set', async () => { expectedCogsResult.ResultsByTime[0].Groups = []; nock('https://ce.us-east-1.amazonaws.com') .post('/') @@ -113,7 +86,7 @@ describe('cogs handler test', () => { const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(404); }); - it('test for empty data set', async () => { + it('Should test for empty data set', async () => { expectedCogsResult.ResultsByTime = []; nock('https://ce.us-east-1.amazonaws.com') .post('/') @@ -121,4 +94,29 @@ describe('cogs handler test', () => { const result = await main(messageBodyJson, context); expect(result.status).to.be.equal(404); }); + it('Should fail, when missing startDate input', async () => { + delete messageBodyJson.startDate; + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(400, { message: 'Start time is invalid. Valid format is: yyyy-MM-dd' }); + const result = await main(messageBodyJson, context); + expect(result.status).to.be.equal(500); + }); + it('Should fail, when missing endDate input', async () => { + delete messageBodyJson.endDate; + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(400, { message: 'End time is invalid. Valid format is: yyyy-MM-dd' }); + const result = await main(messageBodyJson, context); + expect(result.status).to.be.equal(500); + }); + it('Should fail, when both startDate and endDate are missing in input', async () => { + delete messageBodyJson.startDate; + delete messageBodyJson.endDate; + nock('https://ce.us-east-1.amazonaws.com') + .post('/') + .reply(400, { message: 'Time is invalid. Valid format is: yyyy-MM-dd' }); + const result = await main(messageBodyJson, context); + expect(result.status).to.be.equal(500); + }); }); From ae615f68c7f7931cea919d34869627c6728a2335 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Thu, 1 Feb 2024 17:33:30 -0800 Subject: [PATCH 15/17] Update SQS Data send type to get the correct data in post procesing --- src/cogs/handler.js | 3 ++- test/fixtures/cogs-data.js | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index acf8077a..76cf1a87 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -126,7 +126,8 @@ export default async function auditCOGs(message, context) { Object.keys(usageCost).forEach(async (monthYear) => { await sqs.sendMessage(monthYear, { type, - ...usageCost[monthYear], + monthYear, + usageCost: usageCost[monthYear], }); }); diff --git a/test/fixtures/cogs-data.js b/test/fixtures/cogs-data.js index ddf2d8c5..cfd7435e 100644 --- a/test/fixtures/cogs-data.js +++ b/test/fixtures/cogs-data.js @@ -111,10 +111,13 @@ export const expectedCogsResult = { }; export const expectedCOGSValue = { type: 'cogs', - LAMBDA: '14.67', - 'SECRETS MANAGER': '0.09', - DYNAMODB: '0.10', - SQS: '0.17', - S3: '0.00', - CLOUDWATCH: '0.05', + monthYear: 'Dec-23', + usageCost: { + LAMBDA: '14.67', + 'SECRETS MANAGER': '0.09', + DYNAMODB: '0.10', + SQS: '0.17', + S3: '0.00', + CLOUDWATCH: '0.05', + }, }; From 59e451b76bd9e66e80693905196a6bba5ff54fda Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Mon, 5 Feb 2024 10:29:50 +0100 Subject: [PATCH 16/17] Updated util dependency --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 0b1324b9..c2b00cf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,8 @@ "@adobe/spacecat-shared-data-access": "1.14.0", "@adobe/spacecat-shared-http-utils": "1.1.3", "@adobe/spacecat-shared-rum-api-client": "1.4.3", - "@aws-sdk/client-cost-explorer": "3.499.0", "@adobe/spacecat-shared-utils": "1.7.4", + "@aws-sdk/client-cost-explorer": "3.499.0", "@aws-sdk/client-sqs": "3.504.0", "diff": "5.1.0", "urijs": "1.19.11" From bfdd3f0fed493b7bd2c0d865f3bf1b2c2c99f55b Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Mon, 5 Feb 2024 11:47:57 +0100 Subject: [PATCH 17/17] Added AUDIT_RESULTS_QUEUE_URL --- src/cogs/handler.js | 5 ++++- test/audits/cogs.test.js | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cogs/handler.js b/src/cogs/handler.js index 76cf1a87..fd37929e 100644 --- a/src/cogs/handler.js +++ b/src/cogs/handler.js @@ -112,6 +112,9 @@ export default async function auditCOGs(message, context) { const { type, startDate, endDate } = message; const { log, sqs } = context; const { region } = context.runtime; + const { + AUDIT_RESULTS_QUEUE_URL: queueUrl, + } = context.env; log.info(`Fetching Cost Usage from ${startDate} to ${endDate}`); try { @@ -124,7 +127,7 @@ export default async function auditCOGs(message, context) { return notFound('No Cost Usage found'); } Object.keys(usageCost).forEach(async (monthYear) => { - await sqs.sendMessage(monthYear, { + await sqs.sendMessage(queueUrl, { type, monthYear, usageCost: usageCost[monthYear], diff --git a/test/audits/cogs.test.js b/test/audits/cogs.test.js index e9ee9bb7..f00bf6e8 100644 --- a/test/audits/cogs.test.js +++ b/test/audits/cogs.test.js @@ -28,7 +28,6 @@ const sandbox = sinon.createSandbox(); describe('COGS handler test', () => { let context; let messageBodyJson; - const COGS_RESULT_MONTH_YEAR = 'Dec-23'; beforeEach('setup', () => { messageBodyJson = { type: 'cogs', @@ -40,6 +39,9 @@ describe('COGS handler test', () => { runtime: { region: 'us-east-1', }, + env: { + AUDIT_RESULTS_QUEUE_URL: 'queueUrl', + }, sqs: { sendMessage: sandbox.stub().resolves(), }, @@ -57,7 +59,7 @@ describe('COGS handler test', () => { expect(result.status).to.be.equal(204); expect(context.sqs.sendMessage).to.have.been.calledOnce; expect(context.sqs.sendMessage).to.have.been - .calledWith(COGS_RESULT_MONTH_YEAR, expectedCOGSValue); + .calledWith(context.env.AUDIT_RESULTS_QUEUE_URL, expectedCOGSValue); }); it('Should test for new service data', async () => { expectedCogsResult.ResultsByTime[0].Groups.push({