fix: migrate domain plugin to AWS SDK v3 (#13198)

This commit is contained in:
Tomasz Czubocha 2026-01-07 17:08:17 +00:00 committed by GitHub
parent be57f60f6e
commit ee3f3cb96b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 226 additions and 371 deletions

192
package-lock.json generated
View File

@ -800,6 +800,7 @@
"resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.953.0.tgz",
"integrity": "sha512-QeSFxXgRjpr8M2wiLUsgg+mXEDtdhcuMnBWbXyjqUwca38pLEFJzJdFyOGul9RoQ2ICseuAy2/RZt0Ri1UgeZQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
@ -1317,6 +1318,7 @@
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.953.0.tgz",
"integrity": "sha512-Lxxdhq5nt6ONulu1UHbIS0tVIar7itXv1m4TJfkVzuSm/yQzxIwnFkLtgW/0P5KIE+FS1yUoE2lS+dJBS1PLFw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@aws-crypto/sha1-browser": "5.2.0",
"@aws-crypto/sha256-browser": "5.2.0",
@ -2518,6 +2520,7 @@
"version": "7.26.10",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
@ -3435,7 +3438,6 @@
"version": "8.57.0",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@ -3548,7 +3550,6 @@
"version": "0.11.14",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.2",
"debug": "^4.3.1",
@ -3851,6 +3852,7 @@
"version": "29.7.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/environment": "^29.7.0",
"@jest/expect": "^29.7.0",
@ -4480,6 +4482,7 @@
"node_modules/@octokit/core": {
"version": "6.1.4",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^5.0.0",
"@octokit/graphql": "^8.1.2",
@ -5962,6 +5965,7 @@
"version": "8.14.1",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -6945,16 +6949,6 @@
"version": "3.0.2",
"license": "Apache-2.0"
},
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"license": "MIT",
@ -7120,6 +7114,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@ -7185,7 +7180,6 @@
"version": "3.3.0",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
},
@ -7197,7 +7191,6 @@
"version": "5.1.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"semver": "^7.0.0"
}
@ -7906,16 +7899,6 @@
"node": ">=0.12"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 12"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
"license": "MIT",
@ -8863,6 +8846,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@ -9183,6 +9167,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -9259,13 +9244,6 @@
"type": "^2.7.2"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT",
"optional": true
},
"node_modules/fast-content-type-parse": {
"version": "2.0.1",
"funding": [
@ -9378,30 +9356,6 @@
"bser": "2.1.1"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fetch-retry": {
"version": "6.0.0",
"license": "MIT"
@ -9563,19 +9517,6 @@
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"optional": true,
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"license": "MIT",
@ -9656,38 +9597,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gaxios": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2",
"rimraf": "^5.0.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/gaxios/node_modules/rimraf": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
"license": "ISC",
"optional": true,
"dependencies": {
"glob": "^10.3.7"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"dev": true,
@ -9910,16 +9819,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/google-logging-utils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"license": "MIT",
@ -10314,7 +10213,6 @@
"version": "3.2.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"builtin-modules": "^3.3.0"
},
@ -10806,6 +10704,7 @@
"version": "29.7.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@ -11449,16 +11348,6 @@
"node": ">=6"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"dev": true,
@ -12431,46 +12320,6 @@
"version": "1.1.0",
"license": "ISC"
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"optional": true,
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"dev": true,
@ -15188,16 +15037,6 @@
"makeerror": "1.0.12"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@ -15435,6 +15274,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
@ -15668,6 +15508,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@ -15881,6 +15722,7 @@
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-acm": "3.953.0",
"@aws-sdk/client-api-gateway": "3.953.0",
"@aws-sdk/client-apigatewayv2": "3.953.0",
"@aws-sdk/client-cloudformation": "3.953.0",
@ -15890,12 +15732,15 @@
"@aws-sdk/client-iam": "3.953.0",
"@aws-sdk/client-iot": "3.953.0",
"@aws-sdk/client-lambda": "3.953.0",
"@aws-sdk/client-route-53": "3.953.0",
"@aws-sdk/client-s3": "3.953.0",
"@aws-sdk/client-sts": "3.953.0",
"@aws-sdk/credential-providers": "3.953.0",
"@aws-sdk/lib-storage": "3.953.0",
"@iarna/toml": "^2.2.5",
"@serverlessinc/sf-core": "*",
"@smithy/node-http-handler": "^4.4.5",
"@smithy/util-retry": "^4.2.6",
"adm-zip": "^0.5.16",
"ajv": "8.17.1",
"ajv-formats": "2.1.1",
@ -16195,7 +16040,7 @@
},
"packages/sf-core": {
"name": "@serverlessinc/sf-core",
"version": "4.29.3",
"version": "4.29.4",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-cloudformation": "3.953.0",
@ -16258,7 +16103,7 @@
},
"packages/sf-core-installer": {
"name": "serverless",
"version": "4.29.3",
"version": "4.29.4",
"hasInstallScript": true,
"dependencies": {
"axios": "^1.13.2",
@ -16314,6 +16159,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",

View File

@ -1,10 +1,21 @@
import AWS from 'aws-sdk'
import {
ACMClient,
CertificateStatus,
ListCertificatesCommand,
RequestCertificateCommand,
DescribeCertificateCommand,
} from '@aws-sdk/client-acm'
import { addProxyToAwsClient } from '@serverless/util'
import Globals from '../globals.js'
import { getAWSPagedResults, sleep } from '../utils.js'
import Logging from '../logging.js'
import { ServerlessError, ServerlessErrorCodes } from '@serverless/util'
const certStatuses = ['PENDING_VALIDATION', 'ISSUED', 'INACTIVE']
const certStatuses = [
CertificateStatus.PENDING_VALIDATION,
CertificateStatus.ISSUED,
CertificateStatus.INACTIVE,
]
class ACMWrapper {
constructor(credentials, endpointType) {
@ -12,15 +23,14 @@ class ACMWrapper {
const config = {
region: isEdge ? Globals.defaultRegion : Globals.getRegion(),
endpoint: Globals.getServiceEndpoint('acm'),
...Globals.getRetryStrategy(),
...Globals.getRequestHandler(),
retryStrategy: Globals.getRetryStrategy(),
}
if (credentials) {
config.credentials = credentials
}
this.acm = new AWS.ACM(config)
this.acm = addProxyToAwsClient(new ACMClient(config))
}
async getCertArn(domain) {
@ -30,11 +40,10 @@ class ACMWrapper {
try {
const certificates = await getAWSPagedResults(
this.acm,
'listCertificates',
'CertificateSummaryList',
'NextToken',
'NextToken',
{ CertificateStatuses: certStatuses },
new ListCertificatesCommand({ CertificateStatuses: certStatuses }),
)
// enhancement idea: weight the choice of cert so longer expires
// and RenewalEligibility = ELIGIBLE is more preferable
@ -50,7 +59,9 @@ class ACMWrapper {
certificateName,
)
}
Logging.logInfo(`Found a certificate ARN: '${certificateArn}'`)
if (certificateArn) {
Logging.logInfo(`Found existing certificate ARN: '${certificateArn}'`)
}
} catch (err) {
throw new ServerlessError(
`Could not search certificates in Certificate Manager.\n${err.message}`,
@ -129,10 +140,9 @@ class ACMWrapper {
const params = {
DomainName: domainName,
ValidationMethod: 'DNS',
// SubjectAlternativeNames: [],
}
const result = await this.acm.requestCertificate(params).promise()
const result = await this.acm.send(new RequestCertificateCommand(params))
Logging.logInfo(
`Certificate created with ARN: '${result.CertificateArn}'`,
)
@ -168,7 +178,9 @@ class ACMWrapper {
while (Date.now() - startTime < maxWaitTimeMs) {
try {
const params = { CertificateArn: certificateArn }
const result = await this.acm.describeCertificate(params).promise()
const result = await this.acm.send(
new DescribeCertificateCommand(params),
)
if (
result.Certificate.DomainValidationOptions &&
@ -193,7 +205,8 @@ class ACMWrapper {
} catch (err) {
throw new ServerlessError(
`Failed to get validation records for certificate '${certificateArn}': ${err.message}`,
ServerlessErrorCodes.domains.ACM_CERTIFICATE_VALIDATION_RECORDS_FAILED,
ServerlessErrorCodes.domains
.ACM_CERTIFICATE_VALIDATION_RECORDS_FAILED,
{ originalMessage: err.message },
)
}
@ -227,7 +240,9 @@ class ACMWrapper {
while (Date.now() - startTime < maxWaitTimeMs) {
try {
const params = { CertificateArn: certificateArn }
const result = await this.acm.describeCertificate(params).promise()
const result = await this.acm.send(
new DescribeCertificateCommand(params),
)
if (result.Certificate.Status === 'ISSUED') {
Logging.logInfo('Certificate validation completed successfully!')

View File

@ -1,10 +1,19 @@
/**
* Wrapper class for AWS APIGateway provider
*/
import DomainConfig from '../models/domain-config.js'
import {
APIGatewayClient,
CreateDomainNameCommand,
GetDomainNameCommand,
DeleteDomainNameCommand,
CreateBasePathMappingCommand,
GetBasePathMappingsCommand,
UpdateBasePathMappingCommand,
DeleteBasePathMappingCommand,
} from '@aws-sdk/client-api-gateway'
import { addProxyToAwsClient } from '@serverless/util'
import DomainInfo from '../models/domain-info.js'
import Globals from '../globals.js'
import AWS from 'aws-sdk'
import ApiGatewayMap from '../models/api-gateway-map.js'
import APIGatewayBase from '../models/apigateway-base.js'
import Logging from '../logging.js'
@ -17,15 +26,14 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
const config = {
region: Globals.getRegion(),
endpoint: Globals.getServiceEndpoint('apigateway'),
...Globals.getRetryStrategy(),
...Globals.getRequestHandler(),
retryStrategy: Globals.getRetryStrategy(),
}
if (credentials) {
config.credentials = credentials
}
this.apiGateway = new AWS.APIGateway(config)
this.apiGateway = addProxyToAwsClient(new APIGatewayClient(config))
}
async createCustomDomain(domain) {
@ -62,9 +70,9 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
}
try {
const domainInfo = await this.apiGateway
.createDomainName(params)
.promise()
const domainInfo = await this.apiGateway.send(
new CreateDomainNameCommand(params),
)
return new DomainInfo(domainInfo)
} catch (err) {
throw new ServerlessError(
@ -84,31 +92,33 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
async getCustomDomain(domain, silent = true) {
// Make API call
try {
const domainInfo = await this.apiGateway
.getDomainName({
const domainInfo = await this.apiGateway.send(
new GetDomainNameCommand({
domainName: domain.givenDomainName,
})
.promise()
}),
)
return new DomainInfo(domainInfo)
} catch (err) {
if (!err.statusCode || err.statusCode !== 404 || !silent) {
const statusCode = err.$metadata?.httpStatusCode
if (!statusCode || statusCode !== 404 || !silent) {
throw new ServerlessError(
`V1 - Unable to fetch information about '${domain.givenDomainName}':\n${err.message}`,
ServerlessErrorCodes.domains.API_GATEWAY_CUSTOM_DOMAIN_FETCH_FAILED,
{ originalMessage: err.message },
)
}
Logging.logWarning(`V1 - '${domain.givenDomainName}' does not exist.`)
}
}
async deleteCustomDomain(domain) {
// Make API call
try {
await this.apiGateway
.deleteDomainName({
await this.apiGateway.send(
new DeleteDomainNameCommand({
domainName: domain.givenDomainName,
})
.promise()
}),
)
} catch (err) {
throw new ServerlessError(
`V1 - Failed to delete custom domain '${domain.givenDomainName}':\n${err.message}`,
@ -120,21 +130,22 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
async createBasePathMapping(domain) {
try {
await this.apiGateway
.createBasePathMapping({
await this.apiGateway.send(
new CreateBasePathMappingCommand({
basePath: domain.basePath,
domainName: domain.givenDomainName,
restApiId: domain.apiId,
stage: domain.stage,
})
.promise()
}),
)
Logging.logInfo(
`V1 - Created API mapping '${Logging.formatBasePathForDisplay(domain.basePath)}' for '${domain.givenDomainName}'`,
)
} catch (err) {
throw new ServerlessError(
`V1 - Unable to create base path mapping for '${domain.givenDomainName}':\n${err.message}`,
ServerlessErrorCodes.domains.API_GATEWAY_BASE_PATH_MAPPING_CREATION_FAILED,
ServerlessErrorCodes.domains
.API_GATEWAY_BASE_PATH_MAPPING_CREATION_FAILED,
{ originalMessage: err.message },
)
}
@ -144,13 +155,12 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
try {
const items = await getAWSPagedResults(
this.apiGateway,
'getBasePathMappings',
'items',
'position',
'position',
{
new GetBasePathMappingsCommand({
domainName: domain.givenDomainName,
},
}),
)
return items.map((item) => {
return new ApiGatewayMap(
@ -174,8 +184,8 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
Logging.logInfo(`V1 - Updating API mapping from '${Logging.formatBasePathForDisplay(domain.apiMapping.basePath)}'
to '${Logging.formatBasePathForDisplay(domain.basePath)}' for '${domain.givenDomainName}'`)
try {
await this.apiGateway
.updateBasePathMapping({
await this.apiGateway.send(
new UpdateBasePathMappingCommand({
basePath: domain.apiMapping.basePath,
domainName: domain.givenDomainName,
patchOperations: [
@ -185,12 +195,13 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
value: domain.basePath,
},
],
})
.promise()
}),
)
} catch (err) {
throw new ServerlessError(
`V1 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}`,
ServerlessErrorCodes.domains.API_GATEWAY_BASE_PATH_MAPPING_UPDATE_FAILED,
ServerlessErrorCodes.domains
.API_GATEWAY_BASE_PATH_MAPPING_UPDATE_FAILED,
{ originalMessage: err.message },
)
}
@ -198,19 +209,20 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
async deleteBasePathMapping(domain) {
try {
await this.apiGateway
.deleteBasePathMapping({
await this.apiGateway.send(
new DeleteBasePathMappingCommand({
basePath: domain.apiMapping.basePath,
domainName: domain.givenDomainName,
})
.promise()
}),
)
Logging.logInfo(
`V1 - Removed '${Logging.formatBasePathForDisplay(domain.apiMapping.basePath)}' base path mapping`,
)
} catch (err) {
throw new ServerlessError(
`V1 - Unable to remove base path mapping for '${domain.givenDomainName}':\n${err.message}`,
ServerlessErrorCodes.domains.API_GATEWAY_BASE_PATH_MAPPING_DELETION_FAILED,
ServerlessErrorCodes.domains
.API_GATEWAY_BASE_PATH_MAPPING_DELETION_FAILED,
{ originalMessage: err.message },
)
}

View File

@ -1,12 +1,21 @@
/**
* Wrapper class for AWS APIGatewayV2 provider
*/
import DomainConfig from '../models/domain-config.js'
import {
ApiGatewayV2Client,
CreateDomainNameCommand,
GetDomainNameCommand,
DeleteDomainNameCommand,
CreateApiMappingCommand,
GetApiMappingsCommand,
UpdateApiMappingCommand,
DeleteApiMappingCommand,
} from '@aws-sdk/client-apigatewayv2'
import { addProxyToAwsClient } from '@serverless/util'
import DomainInfo from '../models/domain-info.js'
import Globals from '../globals.js'
import ApiGatewayMap from '../models/api-gateway-map.js'
import APIGatewayBase from '../models/apigateway-base.js'
import AWS from 'aws-sdk'
import Logging from '../logging.js'
import { getAWSPagedResults } from '../utils.js'
import { ServerlessError, ServerlessErrorCodes } from '@serverless/util'
@ -17,15 +26,14 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
const config = {
region: Globals.getRegion(),
endpoint: Globals.getServiceEndpoint('apigatewayv2'),
...Globals.getRetryStrategy(),
...Globals.getRequestHandler(),
retryStrategy: Globals.getRetryStrategy(),
}
if (credentials) {
config.credentials = credentials
}
this.apiGateway = new AWS.ApiGatewayV2(config)
this.apiGateway = addProxyToAwsClient(new ApiGatewayV2Client(config))
}
/**
@ -64,9 +72,9 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
}
try {
const domainInfo = await this.apiGateway
.createDomainName(params)
.promise()
const domainInfo = await this.apiGateway.send(
new CreateDomainNameCommand(params),
)
return new DomainInfo(domainInfo)
} catch (err) {
throw new ServerlessError(
@ -86,14 +94,15 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
async getCustomDomain(domain, silent = true) {
// Make API call
try {
const domainInfo = await this.apiGateway
.getDomainName({
const domainInfo = await this.apiGateway.send(
new GetDomainNameCommand({
DomainName: domain.givenDomainName,
})
.promise()
}),
)
return new DomainInfo(domainInfo)
} catch (err) {
if (!err.statusCode || err.statusCode !== 404 || !silent) {
const statusCode = err.$metadata?.httpStatusCode
if (!statusCode || statusCode !== 404 || !silent) {
throw new ServerlessError(
`V2 - Unable to fetch information about '${domain.givenDomainName}':\n${err.message}`,
ServerlessErrorCodes.domains.API_GATEWAY_CUSTOM_DOMAIN_FETCH_FAILED,
@ -112,11 +121,11 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
async deleteCustomDomain(domain) {
// Make API call
try {
await this.apiGateway
.deleteDomainName({
await this.apiGateway.send(
new DeleteDomainNameCommand({
DomainName: domain.givenDomainName,
})
.promise()
}),
)
} catch (err) {
throw new ServerlessError(
`V2 - Failed to delete custom domain '${domain.givenDomainName}':\n${err.message}`,
@ -133,8 +142,8 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
*/
async createBasePathMapping(domain) {
try {
await this.apiGateway
.createApiMapping({
await this.apiGateway.send(
new CreateApiMappingCommand({
ApiId: domain.apiId,
ApiMappingKey: domain.basePath,
DomainName: domain.givenDomainName,
@ -142,15 +151,16 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
domain.apiType === Globals.apiTypes.http
? '$default'
: domain.stage,
})
.promise()
}),
)
Logging.logInfo(
`V2 - Created API mapping '${Logging.formatBasePathForDisplay(domain.basePath)}' for '${domain.givenDomainName}'`,
)
} catch (err) {
throw new ServerlessError(
`V2 - Unable to create base path mapping for '${domain.givenDomainName}':\n${err.message}`,
ServerlessErrorCodes.domains.API_GATEWAY_BASE_PATH_MAPPING_CREATION_FAILED,
ServerlessErrorCodes.domains
.API_GATEWAY_BASE_PATH_MAPPING_CREATION_FAILED,
{ originalMessage: err.message },
)
}
@ -165,13 +175,12 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
try {
const items = await getAWSPagedResults(
this.apiGateway,
'getApiMappings',
'Items',
'NextToken',
'NextToken',
{
new GetApiMappingsCommand({
DomainName: domain.givenDomainName,
},
}),
)
return items.map(
(item) =>
@ -198,8 +207,8 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
*/
async updateBasePathMapping(domain) {
try {
await this.apiGateway
.updateApiMapping({
await this.apiGateway.send(
new UpdateApiMappingCommand({
ApiId: domain.apiId,
ApiMappingId: domain.apiMapping.apiMappingId,
ApiMappingKey: domain.basePath,
@ -208,15 +217,16 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
domain.apiType === Globals.apiTypes.http
? '$default'
: domain.stage,
})
.promise()
}),
)
Logging.logInfo(
`V2 - Updated API mapping to '${Logging.formatBasePathForDisplay(domain.basePath)}' for '${domain.givenDomainName}'`,
)
} catch (err) {
throw new ServerlessError(
`V2 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}`,
ServerlessErrorCodes.domains.API_GATEWAY_BASE_PATH_MAPPING_UPDATE_FAILED,
ServerlessErrorCodes.domains
.API_GATEWAY_BASE_PATH_MAPPING_UPDATE_FAILED,
{ originalMessage: err.message },
)
}
@ -229,19 +239,20 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
*/
async deleteBasePathMapping(domain) {
try {
await this.apiGateway
.deleteApiMapping({
await this.apiGateway.send(
new DeleteApiMappingCommand({
ApiMappingId: domain.apiMapping.apiMappingId,
DomainName: domain.givenDomainName,
})
.promise()
}),
)
Logging.logInfo(
`V2 - Removed API Mapping with id: '${domain.apiMapping.apiMappingId}'`,
)
} catch (err) {
throw new ServerlessError(
`V2 - Unable to remove base path mapping for '${domain.givenDomainName}':\n${err.message}`,
ServerlessErrorCodes.domains.API_GATEWAY_BASE_PATH_MAPPING_DELETION_FAILED,
ServerlessErrorCodes.domains
.API_GATEWAY_BASE_PATH_MAPPING_DELETION_FAILED,
{ originalMessage: err.message },
)
}

View File

@ -2,11 +2,20 @@
* Wrapper class for AWS CloudFormation provider
*/
import AWS from 'aws-sdk'
import {
CloudFormationClient,
DescribeStackResourceCommand,
DescribeStacksCommand,
ListExportsCommand,
} from '@aws-sdk/client-cloudformation'
import {
addProxyToAwsClient,
ServerlessError,
ServerlessErrorCodes,
} from '@serverless/util'
import Globals from '../globals.js'
import Logging from '../logging.js'
import { getAWSPagedResults } from '../utils.js'
import { ServerlessError, ServerlessErrorCodes } from '@serverless/util'
class CloudFormationWrapper {
constructor(credentials) {
@ -19,15 +28,14 @@ class CloudFormationWrapper {
const config = {
region: Globals.getRegion(),
endpoint: Globals.getServiceEndpoint('cloudformation'),
...Globals.getRetryStrategy(),
...Globals.getRequestHandler(),
retryStrategy: Globals.getRetryStrategy(),
}
if (credentials) {
config.credentials = credentials
}
this.cloudFormation = new AWS.CloudFormation(config)
this.cloudFormation = addProxyToAwsClient(new CloudFormationClient(config))
}
/**
@ -142,11 +150,10 @@ class CloudFormationWrapper {
async getImportValues(names) {
const exports = await getAWSPagedResults(
this.cloudFormation,
'listExports',
'Exports',
'NextToken',
'NextToken',
{},
new ListExportsCommand({}),
)
// filter Exports by names which we need
const filteredExports = exports.filter(
@ -168,12 +175,12 @@ class CloudFormationWrapper {
*/
async getStack(logicalResourceId, stackName) {
try {
return await this.cloudFormation
.describeStackResource({
return await this.cloudFormation.send(
new DescribeStackResourceCommand({
LogicalResourceId: logicalResourceId,
StackName: stackName,
})
.promise()
}),
)
} catch (err) {
throw new ServerlessError(
`Failed to find CloudFormation resources with an error: ${err.message}\n`,
@ -193,11 +200,10 @@ class CloudFormationWrapper {
// get all stacks from the CloudFormation
const stacks = await getAWSPagedResults(
this.cloudFormation,
'describeStacks',
'Stacks',
'NextToken',
'NextToken',
{},
new DescribeStacksCommand({}),
)
// filter stacks by given stackName and check by nested stack RootId

View File

@ -1,21 +1,16 @@
import {
Route53Client,
ChangeResourceRecordSetsCommand,
ListHostedZonesCommand,
ChangeAction,
RRType,
} from '@aws-sdk/client-route-53'
import { addProxyToAwsClient } from '@serverless/util'
import Globals from '../globals.js'
import DomainConfig from '../models/domain-config.js'
import Logging from '../logging.js'
import AWS from 'aws-sdk'
import { getAWSPagedResults } from '../utils.js'
import { ServerlessError, ServerlessErrorCodes } from '@serverless/util'
// Define constants that were imported from v3 SDK
const ChangeAction = {
UPSERT: 'UPSERT',
DELETE: 'DELETE',
}
const RRType = {
A: 'A',
AAAA: 'AAAA',
}
class Route53Wrapper {
constructor(credentials, region) {
// not null and not undefined
@ -23,8 +18,7 @@ class Route53Wrapper {
const config = {
region: region || Globals.getRegion(),
endpoint: serviceEndpoint,
...Globals.getRetryStrategy(),
...Globals.getRequestHandler(),
retryStrategy: Globals.getRetryStrategy(),
}
if (credentials) {
@ -32,7 +26,7 @@ class Route53Wrapper {
}
this.region = config.region
this.route53 = new AWS.Route53(config)
this.route53 = addProxyToAwsClient(new Route53Client(config))
}
/**
@ -57,11 +51,10 @@ class Route53Wrapper {
try {
hostedZones = await getAWSPagedResults(
this.route53,
'listHostedZones',
'HostedZones',
'Marker',
'NextMarker',
{},
new ListHostedZonesCommand({}),
)
Logging.logInfo(
`Found hosted zones list: ${hostedZones.map((zone) => zone.Name)}.`,
@ -114,11 +107,10 @@ class Route53Wrapper {
try {
const hostedZones = await getAWSPagedResults(
this.route53,
'listHostedZones',
'HostedZones',
'Marker',
'NextMarker',
{},
new ListHostedZonesCommand({}),
)
// removing the first part of the domain name, api.test.com => test.com
@ -149,11 +141,10 @@ class Route53Wrapper {
try {
const hostedZones = await getAWSPagedResults(
this.route53,
'listHostedZones',
'HostedZones',
'Marker',
'NextMarker',
{},
new ListHostedZonesCommand({}),
)
for (const record of validationRecords) {
@ -205,12 +196,13 @@ class Route53Wrapper {
HostedZoneId: hostedZoneId,
}
await this.route53.changeResourceRecordSets(params).promise()
await this.route53.send(new ChangeResourceRecordSetsCommand(params))
}
} catch (err) {
throw new ServerlessError(
`Failed to create certificate validation records: ${err.message}`,
ServerlessErrorCodes.route53.ROUTE53_CERTIFICATE_VALIDATION_RECORDS_FAILED,
ServerlessErrorCodes.route53
.ROUTE53_CERTIFICATE_VALIDATION_RECORDS_FAILED,
{ originalMessage: err.message },
)
}
@ -300,7 +292,7 @@ class Route53Wrapper {
}
// Make API call
try {
await this.route53.changeResourceRecordSets(params).promise()
await this.route53.send(new ChangeResourceRecordSetsCommand(params))
} catch (err) {
throw new ServerlessError(
`Failed to ${action} ${recordsToCreate.join(',')} Alias for '${domain.givenDomainName}':\n

View File

@ -1,6 +1,6 @@
import DomainConfig from '../models/domain-config.js'
import { S3Client, HeadObjectCommand } from '@aws-sdk/client-s3'
import { addProxyToAwsClient } from '@serverless/util'
import Logging from '../logging.js'
import AWS from 'aws-sdk'
import Globals from '../globals.js'
import { ServerlessError, ServerlessErrorCodes } from '@serverless/util'
@ -9,15 +9,14 @@ class S3Wrapper {
const config = {
region: Globals.getRegion(),
endpoint: Globals.getServiceEndpoint('s3'),
...Globals.getRetryStrategy(),
...Globals.getRequestHandler(),
retryStrategy: Globals.getRetryStrategy(),
}
if (credentials) {
config.credentials = credentials
}
this.s3 = new AWS.S3(config)
this.s3 = addProxyToAwsClient(new S3Client(config))
}
/**
@ -35,9 +34,10 @@ class S3Wrapper {
}
try {
await this.s3.headObject(params).promise()
await this.s3.send(new HeadObjectCommand(params))
} catch (err) {
if (!err.statusCode || err.statusCode !== 403) {
const statusCode = err.$metadata?.httpStatusCode
if (!statusCode || statusCode !== 403) {
throw new ServerlessError(
`Could not head S3 object at ${domain.tlsTruststoreUri}.\n${err.message}`,
ServerlessErrorCodes.domains.S3_TLS_CERTIFICATE_OBJECT_NOT_FOUND,

View File

@ -1,4 +1,5 @@
import AWS from 'aws-sdk'
import { fromIni } from '@aws-sdk/credential-providers'
import { ConfiguredRetryStrategy } from '@smithy/util-retry'
export default class Globals {
static pluginName = 'domains'
@ -75,28 +76,30 @@ export default class Globals {
}
static getRegion() {
const slsRegion =
Globals.options.region || Globals.serverless.service.provider.region
return slsRegion || Globals.currentRegion || Globals.defaultRegion
return Globals.currentRegion || Globals.defaultRegion
}
/**
* Get credentials for a specific AWS profile using AWS SDK V3
* @param {string} profile - The AWS profile name
* @returns {Promise<object>} - The resolved credentials
*/
static async getProfileCreds(profile) {
const credentials = new AWS.SharedIniFileCredentials({ profile })
return credentials
return fromIni({ profile })()
}
/**
* Get retry strategy for AWS SDK V3 clients
* @param {number} attempts - Maximum retry attempts (default: 5)
* @param {number} delay - Delay in ms per attempt (default: 3000)
* @param {number} backoff - Base backoff in ms (default: 500)
* @returns {ConfiguredRetryStrategy} - The retry strategy instance
*/
static getRetryStrategy(attempts = 5, delay = 3000, backoff = 500) {
return {
retryDelayOptions: {
base: backoff,
customBackoff: (retryCount) => backoff + retryCount * delay,
},
maxRetries: attempts,
}
}
static getRequestHandler() {
// AWS SDK v2 handles proxy configuration automatically
return {}
return new ConfiguredRetryStrategy(
attempts,
// Backoff function: base backoff + delay per attempt
(attempt) => backoff + attempt * delay,
)
}
}

View File

@ -10,7 +10,6 @@ import { sleep } from './utils.js'
import APIGatewayV1Wrapper from './aws/api-gateway-v1-wrapper.js'
import APIGatewayV2Wrapper from './aws/api-gateway-v2-wrapper.js'
import Logging from './logging.js'
import AWS from 'aws-sdk'
import { ServerlessError, ServerlessErrorCodes } from '@serverless/util'
class ServerlessCustomDomain {
@ -81,26 +80,6 @@ class ServerlessCustomDomain {
await this.initAWSRegion()
await this.initAWSResources()
// start of the legacy AWS SDK V2 creds support
// TODO: remove it in case serverless will add V3 support
const domain = this.domains[0]
if (domain) {
try {
await this.getApiGateway(domain).getCustomDomain(domain)
} catch (error) {
if (
error.message.includes(
'Could not load credentials from any providers',
)
) {
Globals.credentials =
await this.serverless.providers.aws.getCredentials()
await this.initAWSResources()
}
}
}
// end of the legacy AWS SDK V2 creds support
return lifecycleFunc.call(this)
}
@ -211,7 +190,8 @@ class ServerlessCustomDomain {
throw new ServerlessError(
"'EDGE' endpointType is not compatible with HTTP APIs. Please change the endpointType to 'REGIONAL' or the apiType to 'rest' or 'websocket'.\n" +
'https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html',
ServerlessErrorCodes.domains.DOMAIN_VALIDATION_INCOMPATIBLE_ENDPOINT_TYPE,
ServerlessErrorCodes.domains
.DOMAIN_VALIDATION_INCOMPATIBLE_ENDPOINT_TYPE,
)
}
} else if (domain.apiType === Globals.apiTypes.websocket) {
@ -219,7 +199,8 @@ class ServerlessCustomDomain {
if (domain.endpointType === Globals.endpointTypes.edge) {
throw new ServerlessError(
"'EDGE' endpointType is not compatible with WebSocket APIs",
ServerlessErrorCodes.domains.DOMAIN_VALIDATION_INCOMPATIBLE_ENDPOINT_TYPE,
ServerlessErrorCodes.domains
.DOMAIN_VALIDATION_INCOMPATIBLE_ENDPOINT_TYPE,
)
}
}
@ -227,30 +208,17 @@ class ServerlessCustomDomain {
}
/**
* Init AWS credentials based on sls `provider.profile`
* Init AWS credentials from the framework (already honors provider.profile and --aws-profile)
*/
async initSLSCredentials() {
const slsProfile =
Globals.options['aws-profile'] ||
Globals.serverless.service.provider.profile
Globals.credentials = slsProfile
? await Globals.getProfileCreds(slsProfile)
: null
Globals.credentials = await this.serverless.providers.aws.getCredentials()
}
/**
* Init AWS current region based on Node options
* Init AWS current region from the framework (already honors --region and provider.region)
*/
async initAWSRegion() {
try {
// For AWS SDK v2, we can get region from config or environment
Globals.currentRegion =
AWS.config.region ||
process.env.AWS_REGION ||
process.env.AWS_DEFAULT_REGION
} catch (err) {
Logging.logInfo('Node region was not found.')
}
Globals.currentRegion = this.serverless.providers.aws.getRegion()
}
/**

View File

@ -42,32 +42,30 @@ function evaluateBoolean(value, defaultValue) {
}
/**
* Iterate through the pages of a AWS SDK v2 response and collect them into a single array
* Iterate through the pages of an AWS SDK v3 response and collect them into a single array
*
* @param {Object} client - The AWS service instance to use to make the calls
* @param {string} methodName - The method name to call on the client
* @param {Object} client - The AWS SDK v3 client instance
* @param {string} resultsKey - The key name in the response that contains the items to return
* @param {string} nextTokenKey - The request key name to append to the request that has the paging token value
* @param {string} nextResponseTokenKey - The response key name that has the next paging token value
* @param {Object} params - Parameters to send in the request
* @param {Object} command - The AWS SDK v3 Command instance to execute
* @returns {Promise<Array>} Promise that resolves to an array of results
*/
async function getAWSPagedResults(
client,
methodName,
resultsKey,
nextTokenKey,
nextResponseTokenKey,
params,
command,
) {
let results = []
let response = await client[methodName](params).promise()
results = results.concat(response[resultsKey] || [])
let response = await client.send(command)
results = results.concat(response[resultsKey] || results)
while (response[nextResponseTokenKey]) {
params[nextTokenKey] = response[nextResponseTokenKey]
response = await client[methodName](params).promise()
results = results.concat(response[resultsKey] || [])
while (nextResponseTokenKey in response && response[nextResponseTokenKey]) {
command.input[nextTokenKey] = response[nextResponseTokenKey]
response = await client.send(command)
results = results.concat(response[resultsKey])
}
return results

View File

@ -19,6 +19,7 @@
],
"main": "lib/serverless.js",
"dependencies": {
"@aws-sdk/client-acm": "3.953.0",
"@aws-sdk/client-api-gateway": "3.953.0",
"@aws-sdk/client-apigatewayv2": "3.953.0",
"@aws-sdk/client-cloudformation": "3.953.0",
@ -28,12 +29,15 @@
"@aws-sdk/client-iam": "3.953.0",
"@aws-sdk/client-iot": "3.953.0",
"@aws-sdk/client-lambda": "3.953.0",
"@aws-sdk/client-route-53": "3.953.0",
"@aws-sdk/client-s3": "3.953.0",
"@aws-sdk/client-sts": "3.953.0",
"@aws-sdk/credential-providers": "3.953.0",
"@aws-sdk/lib-storage": "3.953.0",
"@iarna/toml": "^2.2.5",
"@serverlessinc/sf-core": "*",
"@smithy/node-http-handler": "^4.4.5",
"@smithy/util-retry": "^4.2.6",
"adm-zip": "^0.5.16",
"ajv": "8.17.1",
"ajv-formats": "2.1.1",