diff --git a/CHANGELOG.md b/CHANGELOG.md index 483e7c44..c66cf1c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [7.3.4] - 2023-01-30 + +### Fixed +- Fixed bathPathMapping filtering with the `allowPathMatching` enabled. + ## [7.3.3] - 2023-12-07 ### Changed diff --git a/README.md b/README.md index 5957a360..5ff006ce 100644 --- a/README.md +++ b/README.md @@ -144,35 +144,35 @@ custom: routingPolicy: latency ``` -| Parameter Name | Default Value | Description | -| --- | --- | --- | -| domainName _(Required)_ | | The domain name to be created in API Gateway and Route53 (if enabled) for this API. | -| basePath | `(none)` | The base path that will prepend all API endpoints. | -| stage | Value of `--stage`, or `provider.stage` (serverless will default to `dev` if unset) | The stage to create the domain name for. This parameter allows you to specify a different stage for the domain name than the stage specified for the serverless deployment. | +| Parameter Name | Default Value | Description | +| --- | --- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| domainName _(Required)_ | | The domain name to be created in API Gateway and Route53 (if enabled) for this API. | +| basePath | `(none)` | The base path that will prepend all API endpoints. | +| stage | Value of `--stage`, or `provider.stage` (serverless will default to `dev` if unset) | The stage to create the domain name for. This parameter allows you to specify a different stage for the domain name than the stage specified for the serverless deployment. | | certificateName | Closest match | The name of a specific certificate from Certificate Manager to use with this API. If not specified, the closest match will be used (i.e. for a given domain name `api.example.com`, a certificate for `api.example.com` will take precedence over a `*.example.com` certificate).

Note: Edge-optimized endpoints require that the certificate be located in `us-east-1` to be used with the CloudFront distribution. | -| certificateArn | `(none)` | The arn of a specific certificate from Certificate Manager to use with this API. | -| createRoute53Record | `true` | Toggles whether or not the plugin will create A Alias and AAAA Alias records in Route53 mapping the `domainName` to the generated distribution domain name. If false, does not create a record. | -| createRoute53IPv6Record | `true` | Toggles whether or not the plugin will create an AAAA Alias record in Route53 mapping the `domainName` to the generated distribution domain name. If false, does not create a record. | -| route53Profile | `(none)` | Profile to use for accessing Route53 resources when Route53 records are in a different account | -| route53Region | `(none)` | Region to send Route53 services requests to (only applicable if also using route53Profile option) | -| endpointType | `EDGE` | Defines the endpoint type, accepts `REGIONAL` or `EDGE`. | -| apiType | rest | Defines the api type, accepts `rest`, `http` or `websocket`. | -| tlsTruststoreUri | `undefined` | An Amazon S3 url that specifies the truststore for mutual TLS authentication, for example `s3://bucket-name/key-name`. The truststore can contain certificates from public or private certificate authorities. Be aware mutual TLS is only available for `regional` APIs. | -| tlsTruststoreVersion | `undefined` | The version of the S3 object that contains your truststore. To specify a version, you must have versioning enabled for the S3 bucket. | -| hostedZoneId | | If hostedZoneId is set the route53 record set will be created in the matching zone, otherwise the hosted zone will be figured out from the domainName (hosted zone with matching domain). | +| certificateArn | `(none)` | The arn of a specific certificate from Certificate Manager to use with this API. | +| createRoute53Record | `true` | Toggles whether or not the plugin will create A Alias and AAAA Alias records in Route53 mapping the `domainName` to the generated distribution domain name. If false, does not create a record. | +| createRoute53IPv6Record | `true` | Toggles whether or not the plugin will create an AAAA Alias record in Route53 mapping the `domainName` to the generated distribution domain name. If false, does not create a record. | +| route53Profile | `(none)` | Profile to use for accessing Route53 resources when Route53 records are in a different account | +| route53Region | `(none)` | Region to send Route53 services requests to (only applicable if also using route53Profile option) | +| endpointType | `EDGE` | Defines the endpoint type, accepts `REGIONAL` or `EDGE`. | +| apiType | rest | Defines the api type, accepts `rest`, `http` or `websocket`. | +| tlsTruststoreUri | `undefined` | An Amazon S3 url that specifies the truststore for mutual TLS authentication, for example `s3://bucket-name/key-name`. The truststore can contain certificates from public or private certificate authorities. Be aware mutual TLS is only available for `regional` APIs. | +| tlsTruststoreVersion | `undefined` | The version of the S3 object that contains your truststore. To specify a version, you must have versioning enabled for the S3 bucket. | +| hostedZoneId | | If hostedZoneId is set the route53 record set will be created in the matching zone, otherwise the hosted zone will be figured out from the domainName (hosted zone with matching domain). | | hostedZonePrivate | | If hostedZonePrivate is set to `true` then only private hosted zones will be used for route 53 records. If it is set to `false` then only public hosted zones will be used for route53 records. Setting this parameter is specially useful if you have multiple hosted zones with the same domain name (e.g. a public and a private one). If records need to be set in both private and public hosted zones use splitHorizonDns parameter. | -| splitHorizonDns | `false` | When `hostedZoneId` and `hostedZonePrivate` are not set, setting this to `true` creates route53 records in both private and public hosted zones with matching domain. | -| enabled | true | Sometimes there are stages for which is not desired to have custom domain names. This flag allows the developer to disable the plugin for such cases. Accepts either `boolean` or `string` values and defaults to `true` for backwards compatibility. | -securityPolicy | tls_1_2 | The security policy to apply to the custom domain name. Accepts `tls_1_0` or `tls_1_2`| -allowPathMatching | false | When updating an existing api mapping this will match on the basePath instead of the API ID to find existing mappings for an update. This should only be used when changing API types. For example, migrating a REST API to an HTTP API. See Changing API Types for more information. | -| autoDomain | `false` | Toggles whether or not the plugin will run `create_domain/delete_domain` as part of `sls deploy/remove` so that multiple commands are not required. | -| autoDomainWaitFor | `120` | How long to wait for create_domain to finish before starting deployment if domain does not exist immediately. | -| route53Params | | A set of options to customize Route 53 record creation. If left empty, A and AAAA records with simple routing will be created. If `createRoute53Record` is `false`, anything passed here will be ignored. | -| route53Params:
  routingPolicy | simple | Defines the Route 53 routing policy, accepts `simple`, `latency` or `weighted`. | -| route53Params:
  weight | `200` | Sets the weight for weighted routing. Ignored for `simple` and `latency` routing. | -| route53Params:
  setIdentifier | | A unique identifier for records in a set of Route 53 records with the same domain name. Only relevant for `latency` and `weighted` routing. Defaults to the regional endpoint if not provided. | -| route53Params:
  healthCheckId | | An optional id for a Route 53 health check. If it is failing, Route 53 will stop routing to it. Only relevant for `latency` and `weighted` routing. If it is not provided, no health check will be associated with the record. | -| preserveExternalPathMappings | `false` | When `autoDomain` is set to true, and a deployment is removed, setting this to `true` checks for additional API Gateway base path mappings before automatically deleting the domain, and avoids doing so if they exist. | +| splitHorizonDns | `false` | When `hostedZoneId` and `hostedZonePrivate` are not set, setting this to `true` creates route53 records in both private and public hosted zones with matching domain. | +| enabled | true | Sometimes there are stages for which is not desired to have custom domain names. This flag allows the developer to disable the plugin for such cases. Accepts either `boolean` or `string` values and defaults to `true` for backwards compatibility. | +securityPolicy | tls_1_2 | The security policy to apply to the custom domain name. Accepts `tls_1_0` or `tls_1_2` | +allowPathMatching | false | When updating an existing api mapping this will match on the basePath instead of the API ID to find existing mappings for an update. | +| autoDomain | `false` | Toggles whether or not the plugin will run `create_domain/delete_domain` as part of `sls deploy/remove` so that multiple commands are not required. | +| autoDomainWaitFor | `120` | How long to wait for create_domain to finish before starting deployment if domain does not exist immediately. | +| route53Params | | A set of options to customize Route 53 record creation. If left empty, A and AAAA records with simple routing will be created. If `createRoute53Record` is `false`, anything passed here will be ignored. | +| route53Params:
  routingPolicy | simple | Defines the Route 53 routing policy, accepts `simple`, `latency` or `weighted`. | +| route53Params:
  weight | `200` | Sets the weight for weighted routing. Ignored for `simple` and `latency` routing. | +| route53Params:
  setIdentifier | | A unique identifier for records in a set of Route 53 records with the same domain name. Only relevant for `latency` and `weighted` routing. Defaults to the regional endpoint if not provided. | +| route53Params:
  healthCheckId | | An optional id for a Route 53 health check. If it is failing, Route 53 will stop routing to it. Only relevant for `latency` and `weighted` routing. If it is not provided, no health check will be associated with the record. | +| preserveExternalPathMappings | `false` | When `autoDomain` is set to true, and a deployment is removed, setting this to `true` checks for additional API Gateway base path mappings before automatically deleting the domain, and avoids doing so if they exist. | ## Running diff --git a/package-lock.json b/package-lock.json index 7971f3da..a97d45bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "serverless-domain-manager", - "version": "7.3.2", + "version": "7.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "serverless-domain-manager", - "version": "7.3.2", + "version": "7.3.3", "license": "MIT", "dependencies": { "@aws-sdk/client-acm": "^3.460.0", @@ -26,17 +26,17 @@ }, "devDependencies": { "@types/mocha": "^10.0.6", - "@types/node": "^20.10.0", + "@types/node": "^20.11.10", "@types/randomstring": "^1.1.11", "@types/shelljs": "^0.8.15", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", - "aws-sdk-client-mock": "^3.0.0", - "chai": "^4.3.10", + "aws-sdk-client-mock": "^3.0.1", + "chai": "^4.4.1", "chai-spies": "^1.1.0", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", - "eslint-plugin-import": "^2.29.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.2.0", "mocha": "^10.2.0", @@ -46,7 +46,7 @@ "serverless": "^3.38.0", "serverless-plugin-split-stacks": "^1.13.0", "shelljs": "^0.8.5", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "^5.1.6 && <5.2" }, "engines": { @@ -2408,36 +2408,27 @@ } }, "node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "dependencies": { - "type-detect": "4.0.8" + "@sinonjs/commons": "^3.0.0" } }, "node_modules/@sinonjs/samsam": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", - "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, "dependencies": { "@sinonjs/commons": "^2.0.0", @@ -2445,6 +2436,15 @@ "type-detect": "^4.0.8" } }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/@sinonjs/text-encoding": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", @@ -3182,9 +3182,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", - "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", + "version": "20.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz", + "integrity": "sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -4060,13 +4060,13 @@ } }, "node_modules/aws-sdk-client-mock": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-3.0.0.tgz", - "integrity": "sha512-4mBiWhuLYLZe1+K/iB8eYy5SAZyW2se+Keyh5u9QouMt6/qJ5SRZhss68xvUX5g3ApzROJ06QPRziYHP6buuvQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-3.0.1.tgz", + "integrity": "sha512-9VAzJLl8mz99KP9HjOm/93d8vznRRUTpJooPBOunRdUAnVYopCe9xmMuu7eVemu8fQ+w6rP7o5bBK1kAFkB2KQ==", "dev": true, "dependencies": { "@types/sinon": "^10.0.10", - "sinon": "^14.0.2", + "sinon": "^16.1.3", "tslib": "^2.1.0" } }, @@ -4419,9 +4419,9 @@ ] }, "node_modules/chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -5827,9 +5827,9 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", - "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "dependencies": { "array-includes": "^3.1.7", @@ -5848,7 +5848,7 @@ "object.groupby": "^1.0.1", "object.values": "^1.1.7", "semver": "^6.3.1", - "tsconfig-paths": "^3.14.2" + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" @@ -6599,9 +6599,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true, "funding": [ { @@ -8200,9 +8200,9 @@ } }, "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, "node_modules/jwt-decode": { @@ -8765,25 +8765,25 @@ "dev": true }, "node_modules/nise": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", - "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.7.tgz", + "integrity": "sha512-wWtNUhkT7k58uvWTB/Gy26eA/EJKtPZFVAhEilN5UYVmmGRYOURbejRUyKm0Uu9XVEW7K5nBOZfR8VMB4QR2RQ==", "dev": true, "dependencies": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^10.0.2", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" } }, "node_modules/nise/node_modules/@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, "dependencies": { - "@sinonjs/commons": "^2.0.0" + "@sinonjs/commons": "^3.0.0" } }, "node_modules/node-dir": { @@ -9506,18 +9506,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/path-to-regexp/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", "dev": true }, "node_modules/path-type": { @@ -10571,16 +10562,16 @@ } }, "node_modules/sinon": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", - "integrity": "sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==", + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-16.1.3.tgz", + "integrity": "sha512-mjnWWeyxcAf9nC0bXcPmiDut+oE8HYridTNzBbF98AYVLmWwGRp2ISEpyhYflG1ifILT+eNn3BmKUJPxjXUPlA==", "dev": true, "dependencies": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^9.1.2", - "@sinonjs/samsam": "^7.0.1", - "diff": "^5.0.0", - "nise": "^5.1.2", + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", "supports-color": "^7.2.0" }, "funding": { @@ -10588,6 +10579,15 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/sinon/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11248,9 +11248,9 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -11300,9 +11300,9 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", diff --git a/package.json b/package.json index b7ca5c46..bb66bf99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-domain-manager", - "version": "7.3.3", + "version": "7.3.4", "engines": { "node": ">=14" }, @@ -27,8 +27,8 @@ "main": "dist/src/index.js", "bin": {}, "scripts": { - "integration-basic": "nyc mocha -r ts-node/register --project tsconfig.json --parallel test/integration-tests/basic/basic.test.ts", - "integration-deploy": "nyc mocha -r ts-node/register --project tsconfig.json --parallel test/integration-tests/deploy/deploy.test.ts", + "integration-basic": "nyc mocha -r ts-node/register --project tsconfig.json test/integration-tests/basic/basic.test.ts", + "integration-deploy": "nyc mocha -r ts-node/register --project tsconfig.json test/integration-tests/deploy/deploy.test.ts", "integration-debug": "nyc mocha -r ts-node/register --project tsconfig.json test/integration-tests/debug/debug.test.ts", "test": "find ./test/unit-tests -name '*.test.ts' | xargs nyc mocha -r ts-node/register --project tsconfig.json --timeout 5000 && nyc report --reporter=text-summary", "test:debug": "NODE_OPTIONS='--inspect-brk' mocha -j 1 -r ts-node/register --project tsconfig.json test/unit-tests/index.test.ts", @@ -51,15 +51,15 @@ }, "devDependencies": { "@types/mocha": "^10.0.6", - "@types/node": "^20.10.0", + "@types/node": "^20.11.10", "@types/randomstring": "^1.1.11", "@types/shelljs": "^0.8.15", - "aws-sdk-client-mock": "^3.0.0", - "chai": "^4.3.10", + "aws-sdk-client-mock": "^3.0.1", + "chai": "^4.4.1", "chai-spies": "^1.1.0", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", - "eslint-plugin-import": "^2.29.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.2.0", "@typescript-eslint/parser": "^5.62.0", @@ -71,7 +71,7 @@ "serverless": "^3.38.0", "serverless-plugin-split-stacks": "^1.13.0", "shelljs": "^0.8.5", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "^5.1.6 && <5.2" }, "dependencies": { diff --git a/src/aws/api-gateway-v1-wrapper.ts b/src/aws/api-gateway-v1-wrapper.ts index 1b509f2b..b4f86fdf 100644 --- a/src/aws/api-gateway-v1-wrapper.ts +++ b/src/aws/api-gateway-v1-wrapper.ts @@ -152,6 +152,8 @@ class APIGatewayV1Wrapper extends APIGatewayBase { } public async updateBasePathMapping (domain: DomainConfig): Promise { + Logging.logInfo(`V1 - Updating API mapping from '${domain.apiMapping.basePath}' + to '${domain.basePath}' for '${domain.givenDomainName}'`); try { await this.apiGateway.send(new UpdateBasePathMappingCommand({ basePath: domain.apiMapping.basePath, @@ -163,11 +165,9 @@ class APIGatewayV1Wrapper extends APIGatewayBase { }] } )); - Logging.logInfo(`V1 - Updated API mapping from '${domain.apiMapping.basePath}' - to '${domain.basePath}' for '${domain.givenDomainName}'`); } catch (err) { throw new Error( - `V1 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` + `V1 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` ); } } diff --git a/src/aws/route53-wrapper.ts b/src/aws/route53-wrapper.ts index bf33e772..be105e5c 100644 --- a/src/aws/route53-wrapper.ts +++ b/src/aws/route53-wrapper.ts @@ -94,6 +94,7 @@ class Route53Wrapper { Logging.logInfo(`Skipping ${action === ChangeAction.DELETE ? "removal" : "creation"} of Route53 record.`); return; } + Logging.logInfo(`Creating/updating route53 record for '${domain.givenDomainName}'.`); // Set up parameters const route53HostedZoneId = await this.getRoute53HostedZoneId(domain, domain.hostedZonePrivate); const route53Params = domain.route53Params; diff --git a/src/index.ts b/src/index.ts index c36bc82a..c156e350 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,473 +17,468 @@ import { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } from "@sm import { ChangeAction } from "@aws-sdk/client-route-53"; class ServerlessCustomDomain { - // AWS SDK resources - public apiGatewayV1Wrapper: APIGatewayV1Wrapper; - public apiGatewayV2Wrapper: APIGatewayV2Wrapper; - public cloudFormationWrapper: CloudFormationWrapper; - public s3Wrapper: S3Wrapper; - - // Serverless specific properties - public serverless: ServerlessInstance; - public options: ServerlessOptions; - public commands: object; - public hooks: object; - - // Domain Manager specific properties - public domains: DomainConfig[] = []; - - constructor (serverless: ServerlessInstance, options: ServerlessOptions, v3Utils?: ServerlessUtils) { - this.serverless = serverless; - Globals.serverless = serverless; - - this.options = options; - Globals.options = options; - - if (v3Utils?.log) { - Globals.v3Utils = v3Utils; - } - - /* eslint camelcase: ["error", {allow: ["create_domain", "delete_domain"]}] */ - this.commands = { - create_domain: { - lifecycleEvents: [ - "create", - "initialize" - ], - usage: "Creates a domain using the domain name defined in the serverless file" - }, - delete_domain: { - lifecycleEvents: [ - "delete", - "initialize" - ], - usage: "Deletes a domain using the domain name defined in the serverless file" - } - }; - this.hooks = { - "after:deploy:deploy": this.hookWrapper.bind(this, this.setupBasePathMappings), - "after:info:info": this.hookWrapper.bind(this, this.domainSummaries), - "before:deploy:deploy": this.hookWrapper.bind(this, this.createOrGetDomainForCfOutputs), - "before:remove:remove": this.hookWrapper.bind(this, this.removeBasePathMappings), - "create_domain:create": this.hookWrapper.bind(this, this.createDomains), - "delete_domain:delete": this.hookWrapper.bind(this, this.deleteDomains) - }; + // AWS SDK resources + public apiGatewayV1Wrapper: APIGatewayV1Wrapper; + public apiGatewayV2Wrapper: APIGatewayV2Wrapper; + public cloudFormationWrapper: CloudFormationWrapper; + public s3Wrapper: S3Wrapper; + + // Serverless specific properties + public serverless: ServerlessInstance; + public options: ServerlessOptions; + public commands: object; + public hooks: object; + + // Domain Manager specific properties + public domains: DomainConfig[] = []; + + constructor (serverless: ServerlessInstance, options: ServerlessOptions, v3Utils?: ServerlessUtils) { + this.serverless = serverless; + Globals.serverless = serverless; + + this.options = options; + Globals.options = options; + + if (v3Utils?.log) { + Globals.v3Utils = v3Utils; } - /** - * Wrapper for lifecycle function, initializes variables and checks if enabled. - * @param lifecycleFunc lifecycle function that actually does desired action - */ - public async hookWrapper (lifecycleFunc: any) { - // check if `customDomain` or `customDomains` config exists - this.validateConfigExists(); - // init config variables - this.initializeVariables(); - // Validate the domain configurations - this.validateDomainConfigs(); - // setup AWS resources - await this.initSLSCredentials(); - 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 = this.serverless.providers.aws.getCredentials(); - await this.initAWSResources(); - } + /* eslint camelcase: ["error", {allow: ["create_domain", "delete_domain"]}] */ + this.commands = { + create_domain: { + lifecycleEvents: [ + "create", + "initialize" + ], + usage: "Creates a domain using the domain name defined in the serverless file" + }, + delete_domain: { + lifecycleEvents: [ + "delete", + "initialize" + ], + usage: "Deletes a domain using the domain name defined in the serverless file" + } + }; + this.hooks = { + "after:deploy:deploy": this.hookWrapper.bind(this, this.setupBasePathMappings), + "after:info:info": this.hookWrapper.bind(this, this.domainSummaries), + "before:deploy:deploy": this.hookWrapper.bind(this, this.createOrGetDomainForCfOutputs), + "before:remove:remove": this.hookWrapper.bind(this, this.removeBasePathMappings), + "create_domain:create": this.hookWrapper.bind(this, this.createDomains), + "delete_domain:delete": this.hookWrapper.bind(this, this.deleteDomains) + }; + } + + /** + * Wrapper for lifecycle function, initializes variables and checks if enabled. + * @param lifecycleFunc lifecycle function that actually does desired action + */ + public async hookWrapper (lifecycleFunc: any) { + // check if `customDomain` or `customDomains` config exists + this.validateConfigExists(); + // init config variables + this.initializeVariables(); + // Validate the domain configurations + this.validateDomainConfigs(); + // setup AWS resources + await this.initSLSCredentials(); + 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 = this.serverless.providers.aws.getCredentials(); + await this.initAWSResources(); } } - // end of the legacy AWS SDK V2 creds support - - return lifecycleFunc.call(this); } - - /** - * Validate if the plugin config exists - */ - public validateConfigExists (): void { - // Make sure customDomain configuration exists, stop if not - const config = this.serverless.service.custom; - const domainExists = config && typeof config.customDomain !== "undefined"; - const domainsExists = config && typeof config.customDomains !== "undefined"; - if (typeof config === "undefined" || (!domainExists && !domainsExists)) { - throw new Error(`${Globals.pluginName}: Plugin configuration is missing.`); - } + // end of the legacy AWS SDK V2 creds support + + return lifecycleFunc.call(this); + } + + /** + * Validate if the plugin config exists + */ + public validateConfigExists (): void { + // Make sure customDomain configuration exists, stop if not + const config = this.serverless.service.custom; + const domainExists = config && typeof config.customDomain !== "undefined"; + const domainsExists = config && typeof config.customDomains !== "undefined"; + if (typeof config === "undefined" || (!domainExists && !domainsExists)) { + throw new Error(`${Globals.pluginName}: Plugin configuration is missing.`); } - - /** - * Goes through custom domain property and initializes local variables and cloudformation template - */ - public initializeVariables (): void { - const config = this.serverless.service.custom; - const domainConfig = config.customDomain ? [config.customDomain] : []; - const domainsConfig = config.customDomains || []; - const customDomains: CustomDomain[] = domainConfig.concat(domainsConfig); - - // Loop over the domain configurations and populate the domains array with DomainConfigs - this.domains = []; - customDomains.forEach((domain) => { - // If the key of the item in config is an API type then using per API type domain structure - let isTypeConfigFound = false; - Object.keys(Globals.apiTypes).forEach((apiType) => { - const domainTypeConfig = domain[apiType]; - if (domainTypeConfig) { - domainTypeConfig.apiType = apiType; - this.domains.push(new DomainConfig(domainTypeConfig)); - isTypeConfigFound = true; - } - }); - - if (!isTypeConfigFound) { - this.domains.push(new DomainConfig(domain)); + } + + /** + * Goes through custom domain property and initializes local variables and cloudformation template + */ + public initializeVariables (): void { + const config = this.serverless.service.custom; + const domainConfig = config.customDomain ? [config.customDomain] : []; + const domainsConfig = config.customDomains || []; + const customDomains: CustomDomain[] = domainConfig.concat(domainsConfig); + + // Loop over the domain configurations and populate the domains array with DomainConfigs + this.domains = []; + customDomains.forEach((domain) => { + // If the key of the item in config is an API type then using per API type domain structure + let isTypeConfigFound = false; + Object.keys(Globals.apiTypes).forEach((apiType) => { + const domainTypeConfig = domain[apiType]; + if (domainTypeConfig) { + domainTypeConfig.apiType = apiType; + this.domains.push(new DomainConfig(domainTypeConfig)); + isTypeConfigFound = true; } }); - // Filter inactive domains - this.domains = this.domains.filter((domain) => domain.enabled); - } - - /** - * Validates domain configs to make sure they are valid, ie HTTP api cannot be used with EDGE domain - */ - public validateDomainConfigs () { - this.domains.forEach((domain) => { - if (domain.allowPathMatching) { - Logging.logWarning(`"allowPathMatching" is set for ${domain.givenDomainName}. - This should only be used when migrating a path to a different API type. e.g. REST to HTTP.`); + if (!isTypeConfigFound) { + this.domains.push(new DomainConfig(domain)); + } + }); + + // Filter inactive domains + this.domains = this.domains.filter((domain) => domain.enabled); + } + + /** + * Validates domain configs to make sure they are valid, ie HTTP api cannot be used with EDGE domain + */ + public validateDomainConfigs () { + this.domains.forEach((domain) => { + if (domain.apiType === Globals.apiTypes.rest) { + // No validation for REST API types + } else if (domain.apiType === Globals.apiTypes.http) { + // HTTP APIs do not support edge domains + if (domain.endpointType === Globals.endpointTypes.edge) { + // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html + throw Error( + "'EDGE' endpointType is not compatible with HTTP APIs\n" + + "https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html" + ); } - - if (domain.apiType === Globals.apiTypes.rest) { - // Currently no validation for REST API types - - } else if (domain.apiType === Globals.apiTypes.http) { - // HTTP APIs do not support edge domains - if (domain.endpointType === Globals.endpointTypes.edge) { - // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html - throw Error( - "'EDGE' endpointType is not compatible with HTTP APIs\n" + - "https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html" - ); - } - } else if (domain.apiType === Globals.apiTypes.websocket) { - // Websocket APIs do not support edge domains - if (domain.endpointType === Globals.endpointTypes.edge) { - throw Error("'EDGE' endpointType is not compatible with WebSocket APIs"); - } + } else if (domain.apiType === Globals.apiTypes.websocket) { + // Websocket APIs do not support edge domains + if (domain.endpointType === Globals.endpointTypes.edge) { + throw Error("'EDGE' endpointType is not compatible with WebSocket APIs"); } - }); - } - - /** - * Init AWS credentials based on sls `provider.profile` - */ - public async initSLSCredentials (): Promise { - const slsProfile = Globals.options["aws-profile"] || Globals.serverless.service.provider.profile; - Globals.credentials = slsProfile ? await Globals.getProfileCreds(slsProfile) : null; - } - - /** - * Init AWS current region based on Node options - */ - public async initAWSRegion (): Promise { - try { - Globals.currentRegion = await loadConfig(NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS)(); - } catch (err) { - Logging.logInfo("Node region was not found."); } + }); + } + + /** + * Init AWS credentials based on sls `provider.profile` + */ + public async initSLSCredentials (): Promise { + const slsProfile = Globals.options["aws-profile"] || Globals.serverless.service.provider.profile; + Globals.credentials = slsProfile ? await Globals.getProfileCreds(slsProfile) : null; + } + + /** + * Init AWS current region based on Node options + */ + public async initAWSRegion (): Promise { + try { + Globals.currentRegion = await loadConfig(NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS)(); + } catch (err) { + Logging.logInfo("Node region was not found."); } - - /** - * Setup AWS resources - */ - public async initAWSResources (): Promise { - this.apiGatewayV1Wrapper = new APIGatewayV1Wrapper(Globals.credentials); - this.apiGatewayV2Wrapper = new APIGatewayV2Wrapper(Globals.credentials); - this.cloudFormationWrapper = new CloudFormationWrapper(Globals.credentials); - this.s3Wrapper = new S3Wrapper(Globals.credentials); - } - - public getApiGateway (domain: DomainConfig): APIGatewayBase { - // 1. https://stackoverflow.com/questions/72339224/aws-v1-vs-v2-api-for-listing-apis-on-aws-api-gateway-return-different-data-for-t - // 2. https://aws.amazon.com/blogs/compute/announcing-http-apis-for-amazon-api-gateway/ - // There are currently two API Gateway namespaces for managing API Gateway deployments. - // The API V1 namespace represents REST APIs and API V2 represents WebSocket APIs and the new HTTP APIs. - // You can create an HTTP API by using the AWS Management Console, CLI, APIs, CloudFormation, SDKs, or the Serverless Application Model (SAM). - if (domain.apiType !== Globals.apiTypes.rest) { - return this.apiGatewayV2Wrapper; - } - - // multi-level base path mapping is supported by Gateway V2 - // https://github.com/amplify-education/serverless-domain-manager/issues/558 - // https://aws.amazon.com/blogs/compute/using-multiple-segments-in-amazon-api-gateway-base-path-mapping/ - if (domain.basePath.includes("/")) { - return this.apiGatewayV2Wrapper; - } - - return this.apiGatewayV1Wrapper; + } + + /** + * Setup AWS resources + */ + public async initAWSResources (): Promise { + this.apiGatewayV1Wrapper = new APIGatewayV1Wrapper(Globals.credentials); + this.apiGatewayV2Wrapper = new APIGatewayV2Wrapper(Globals.credentials); + this.cloudFormationWrapper = new CloudFormationWrapper(Globals.credentials); + this.s3Wrapper = new S3Wrapper(Globals.credentials); + } + + public getApiGateway (domain: DomainConfig): APIGatewayBase { + // 1. https://stackoverflow.com/questions/72339224/aws-v1-vs-v2-api-for-listing-apis-on-aws-api-gateway-return-different-data-for-t + // 2. https://aws.amazon.com/blogs/compute/announcing-http-apis-for-amazon-api-gateway/ + // There are currently two API Gateway namespaces for managing API Gateway deployments. + // The API V1 namespace represents REST APIs and API V2 represents WebSocket APIs and the new HTTP APIs. + // You can create an HTTP API by using the AWS Management Console, CLI, APIs, CloudFormation, SDKs, or the Serverless Application Model (SAM). + if (domain.apiType !== Globals.apiTypes.rest) { + return this.apiGatewayV2Wrapper; } - /** - * Lifecycle function to create a domain - * Wraps creating a domain and resource record set - */ - public async createDomains (): Promise { - await Promise.all(this.domains.map(async (domain) => { - await this.createDomain(domain); - })); + // multi-level base path mapping is supported by Gateway V2 + // https://github.com/amplify-education/serverless-domain-manager/issues/558 + // https://aws.amazon.com/blogs/compute/using-multiple-segments-in-amazon-api-gateway-base-path-mapping/ + if (domain.basePath.includes("/")) { + return this.apiGatewayV2Wrapper; } - /** - * Lifecycle function to create a domain - * Wraps creating a domain and resource record set - */ - public async createDomain (domain: DomainConfig): Promise { - const creationProgress = Globals.v3Utils && Globals.v3Utils.progress.get(`create-${domain.givenDomainName}`); - const route53Creds = domain.route53Profile ? await Globals.getProfileCreds(domain.route53Profile) : Globals.credentials; - - const apiGateway = this.getApiGateway(domain); - const route53 = new Route53Wrapper(route53Creds, domain.route53Region); - const acm = new ACMWrapper(Globals.credentials, domain.endpointType); - - domain.domainInfo = await apiGateway.getCustomDomain(domain); - - try { - if (!domain.domainInfo) { - if (domain.tlsTruststoreUri) { - await this.s3Wrapper.assertTlsCertObjectExists(domain); - } - if (!domain.certificateArn) { - domain.certificateArn = await acm.getCertArn(domain); - } - domain.domainInfo = await apiGateway.createCustomDomain(domain); - Logging.logInfo(`Custom domain '${domain.givenDomainName}' was created. - New domains may take up to 40 minutes to be initialized.`); - } else { - Logging.logInfo(`Custom domain '${domain.givenDomainName}' already exists.`); + return this.apiGatewayV1Wrapper; + } + + /** + * Lifecycle function to create a domain + * Wraps creating a domain and resource record set + */ + public async createDomains (): Promise { + await Promise.all(this.domains.map(async (domain) => { + await this.createDomain(domain); + })); + } + + /** + * Lifecycle function to create a domain + * Wraps creating a domain and resource record set + */ + public async createDomain (domain: DomainConfig): Promise { + const creationProgress = Globals.v3Utils && Globals.v3Utils.progress.get(`create-${domain.givenDomainName}`); + const route53Creds = domain.route53Profile ? await Globals.getProfileCreds(domain.route53Profile) : Globals.credentials; + + const apiGateway = this.getApiGateway(domain); + const route53 = new Route53Wrapper(route53Creds, domain.route53Region); + const acm = new ACMWrapper(Globals.credentials, domain.endpointType); + + domain.domainInfo = await apiGateway.getCustomDomain(domain); + + try { + if (!domain.domainInfo) { + if (domain.tlsTruststoreUri) { + await this.s3Wrapper.assertTlsCertObjectExists(domain); } - Logging.logInfo(`Creating/updating route53 record for '${domain.givenDomainName}'.`); - await route53.changeResourceRecordSet(ChangeAction.UPSERT, domain); - } catch (err) { - throw new Error(`Unable to create domain '${domain.givenDomainName}':\n${err.message}`); - } finally { - if (creationProgress) { - creationProgress.remove(); + if (!domain.certificateArn) { + domain.certificateArn = await acm.getCertArn(domain); } + domain.domainInfo = await apiGateway.createCustomDomain(domain); + Logging.logInfo(`Custom domain '${domain.givenDomainName}' was created. + New domains may take up to 40 minutes to be initialized.`); + } else { + Logging.logInfo(`Custom domain '${domain.givenDomainName}' already exists.`); + } + await route53.changeResourceRecordSet(ChangeAction.UPSERT, domain); + } catch (err) { + throw new Error(`Unable to create domain '${domain.givenDomainName}':\n${err.message}`); + } finally { + if (creationProgress) { + creationProgress.remove(); } } - - /** - * Lifecycle function to delete a domain - * Wraps deleting a domain and resource record set - */ - public async deleteDomains (): Promise { - await Promise.all(this.domains.map(async (domain) => { - await this.deleteDomain(domain); - })); + } + + /** + * Lifecycle function to delete a domain + * Wraps deleting a domain and resource record set + */ + public async deleteDomains (): Promise { + await Promise.all(this.domains.map(async (domain) => { + await this.deleteDomain(domain); + })); + } + + /** + * Wraps deleting a domain and resource record set + */ + public async deleteDomain (domain: DomainConfig): Promise { + const apiGateway = this.getApiGateway(domain); + const route53Creds = domain.route53Profile ? await Globals.getProfileCreds(domain.route53Profile) : null; + const route53 = new Route53Wrapper(route53Creds, domain.route53Region); + + domain.domainInfo = await apiGateway.getCustomDomain(domain); + try { + if (domain.domainInfo) { + await apiGateway.deleteCustomDomain(domain); + await route53.changeResourceRecordSet(ChangeAction.DELETE, domain); + domain.domainInfo = null; + Logging.logInfo(`Custom domain ${domain.givenDomainName} was deleted.`); + } else { + Logging.logInfo(`Custom domain ${domain.givenDomainName} does not exist.`); + } + } catch (err) { + throw new Error(`Unable to delete domain '${domain.givenDomainName}':\n${err.message}`); } + } + + /** + * Lifecycle function to createDomain before deploy and add domain info to the CloudFormation stack's Outputs + */ + public async createOrGetDomainForCfOutputs (): Promise { + await Promise.all(this.domains.map(async (domain) => { + if (domain.autoDomain) { + Logging.logInfo("Creating domain name before deploy."); + await this.createDomain(domain); + } - /** - * Wraps deleting a domain and resource record set - */ - public async deleteDomain (domain: DomainConfig): Promise { const apiGateway = this.getApiGateway(domain); - const route53Creds = domain.route53Profile ? await Globals.getProfileCreds(domain.route53Profile) : null; - const route53 = new Route53Wrapper(route53Creds, domain.route53Region); - domain.domainInfo = await apiGateway.getCustomDomain(domain); - try { - if (domain.domainInfo) { - await apiGateway.deleteCustomDomain(domain); - await route53.changeResourceRecordSet(ChangeAction.DELETE, domain); - domain.domainInfo = null; - Logging.logInfo(`Custom domain ${domain.givenDomainName} was deleted.`); - } else { - Logging.logInfo(`Custom domain ${domain.givenDomainName} does not exist.`); - } - } catch (err) { - throw new Error(`Unable to delete domain '${domain.givenDomainName}':\n${err.message}`); - } - } - /** - * Lifecycle function to createDomain before deploy and add domain info to the CloudFormation stack's Outputs - */ - public async createOrGetDomainForCfOutputs (): Promise { - await Promise.all(this.domains.map(async (domain) => { - if (domain.autoDomain) { - Logging.logInfo("Creating domain name before deploy."); - await this.createDomain(domain); - } - - const apiGateway = this.getApiGateway(domain); - domain.domainInfo = await apiGateway.getCustomDomain(domain); - - if (domain.autoDomain) { - const atLeastOneDoesNotExist = () => this.domains.some((d) => !d.domainInfo); - const maxWaitFor = parseInt(domain.autoDomainWaitFor, 10) || 120; - const pollInterval = 3; - for (let i = 0; i * pollInterval < maxWaitFor && atLeastOneDoesNotExist() === true; i++) { - Logging.logInfo(` + if (domain.autoDomain) { + const atLeastOneDoesNotExist = () => this.domains.some((d) => !d.domainInfo); + const maxWaitFor = parseInt(domain.autoDomainWaitFor, 10) || 120; + const pollInterval = 3; + for (let i = 0; i * pollInterval < maxWaitFor && atLeastOneDoesNotExist() === true; i++) { + Logging.logInfo(` Poll #${i + 1}: polling every ${pollInterval} seconds for domain to exist or until ${maxWaitFor} seconds have elapsed before starting deployment `); - await sleep(pollInterval); - domain.domainInfo = await apiGateway.getCustomDomain(domain); - } + await sleep(pollInterval); + domain.domainInfo = await apiGateway.getCustomDomain(domain); } - this.addOutputs(domain); - })); - } - - /** - * Lifecycle function to create basepath mapping - * Wraps creation of basepath mapping and adds domain name info as output to cloudformation stack - */ - public async setupBasePathMappings (): Promise { - await Promise.all(this.domains.map(async (domain) => { - domain.apiId = await this.cloudFormationWrapper.findApiId(domain.apiType); - - const apiGateway = this.getApiGateway(domain); - const mappings = await apiGateway.getBasePathMappings(domain); + } + this.addOutputs(domain); + })); + } + + /** + * Lifecycle function to create basepath mapping + * Wraps creation of basepath mapping and adds domain name info as output to cloudformation stack + */ + public async setupBasePathMappings (): Promise { + await Promise.all(this.domains.map(async (domain) => { + domain.apiId = await this.cloudFormationWrapper.findApiId(domain.apiType); - const filteredMappings = mappings.filter((mapping) => { - return mapping.apiId === domain.apiId || ( - mapping.basePath === domain.basePath && domain.allowPathMatching - ); - }); - domain.apiMapping = filteredMappings ? filteredMappings[0] : null; - domain.domainInfo = await apiGateway.getCustomDomain(domain); + const apiGateway = this.getApiGateway(domain); + const mappings = await apiGateway.getBasePathMappings(domain); - if (!domain.apiMapping) { - await apiGateway.createBasePathMapping(domain); - } else { - await apiGateway.updateBasePathMapping(domain); + const filteredMappings = mappings.filter((mapping) => { + if (domain.allowPathMatching) { + return mapping.basePath === domain.basePath; } - })).finally(() => { - Logging.printDomainSummary(this.domains); + return mapping.apiId === domain.apiId; }); - } + domain.apiMapping = filteredMappings ? filteredMappings[0] : null; + domain.domainInfo = await apiGateway.getCustomDomain(domain); - /** - * Lifecycle function to delete basepath mapping - * Wraps deletion of basepath mapping - */ - public async removeBasePathMappings (): Promise { - await Promise.all(this.domains.map(async (domain) => { - let externalBasePathExists = false; - try { - domain.apiId = await this.cloudFormationWrapper.findApiId(domain.apiType); - // Unable to find the corresponding API, manual clean up will be required - if (!domain.apiId) { - Logging.logInfo(`Unable to find corresponding API for '${domain.givenDomainName}', + if (!domain.apiMapping) { + await apiGateway.createBasePathMapping(domain); + } else { + await apiGateway.updateBasePathMapping(domain); + } + })).finally(() => { + Logging.printDomainSummary(this.domains); + }); + } + + /** + * Lifecycle function to delete basepath mapping + * Wraps deletion of basepath mapping + */ + public async removeBasePathMappings (): Promise { + await Promise.all(this.domains.map(async (domain) => { + let externalBasePathExists = false; + try { + domain.apiId = await this.cloudFormationWrapper.findApiId(domain.apiType); + // Unable to find the corresponding API, manual clean up will be required + if (!domain.apiId) { + Logging.logInfo(`Unable to find corresponding API for '${domain.givenDomainName}', API Mappings may need to be manually removed.`); - } else { - const apiGateway = this.getApiGateway(domain); - const mappings = await apiGateway.getBasePathMappings(domain); - const filteredMappings = mappings.filter((mapping) => { - return mapping.apiId === domain.apiId || ( - mapping.basePath === domain.basePath && domain.allowPathMatching - ); - }); - if (domain.preserveExternalPathMappings) { - externalBasePathExists = mappings.length > filteredMappings.length; - } - domain.apiMapping = filteredMappings ? filteredMappings[0] : null; - if (domain.apiMapping) { - await apiGateway.deleteBasePathMapping(domain); - } else { - Logging.logWarning( - `Api mapping was not found for '${domain.givenDomainName}'. Skipping base path deletion.` - ); + } else { + const apiGateway = this.getApiGateway(domain); + const mappings = await apiGateway.getBasePathMappings(domain); + const filteredMappings = mappings.filter((mapping) => { + if (domain.allowPathMatching) { + return mapping.basePath === domain.basePath; } + return mapping.apiId === domain.apiId; + }); + if (domain.preserveExternalPathMappings) { + externalBasePathExists = mappings.length > filteredMappings.length; } - } catch (err) { - if (err.message.indexOf("Failed to find CloudFormation") > -1) { - Logging.logWarning(`Unable to find Cloudformation Stack for ${domain.givenDomainName}, - API Mappings may need to be manually removed.`); + domain.apiMapping = filteredMappings ? filteredMappings[0] : null; + if (domain.apiMapping) { + await apiGateway.deleteBasePathMapping(domain); } else { Logging.logWarning( - `Unable to remove base path mappings for '${domain.givenDomainName}':\n${err.message}` + `Api mapping was not found for '${domain.givenDomainName}'. Skipping base path deletion.` ); } } - - if (domain.autoDomain === true && !externalBasePathExists) { - Logging.logInfo("Deleting domain name after removing base path mapping."); - await this.deleteDomain(domain); + } catch (err) { + if (err.message.indexOf("Failed to find CloudFormation") > -1) { + Logging.logWarning(`Unable to find Cloudformation Stack for ${domain.givenDomainName}, + API Mappings may need to be manually removed.`); + } else { + Logging.logWarning( + `Unable to remove base path mappings for '${domain.givenDomainName}':\n${err.message}` + ); } - })); - } - - /** - * Lifecycle function to print domain summary - * Wraps printing of all domain manager related info - */ - public async domainSummaries (): Promise { - await Promise.all(this.domains.map(async (domain) => { - const apiGateway = this.getApiGateway(domain); - domain.domainInfo = await apiGateway.getCustomDomain(domain); - })).finally(() => { - Logging.printDomainSummary(this.domains); - }); - } + } - /** - * Adds the domain name and distribution domain name to the CloudFormation outputs - */ - public addOutputs (domain: DomainConfig): void { - const service = this.serverless.service; - if (!service.provider.compiledCloudFormationTemplate.Outputs) { - service.provider.compiledCloudFormationTemplate.Outputs = {}; + if (domain.autoDomain === true && !externalBasePathExists) { + Logging.logInfo("Deleting domain name after removing base path mapping."); + await this.deleteDomain(domain); } + })); + } + + /** + * Lifecycle function to print domain summary + * Wraps printing of all domain manager related info + */ + public async domainSummaries (): Promise { + await Promise.all(this.domains.map(async (domain) => { + const apiGateway = this.getApiGateway(domain); + domain.domainInfo = await apiGateway.getCustomDomain(domain); + })).finally(() => { + Logging.printDomainSummary(this.domains); + }); + } + + /** + * Adds the domain name and distribution domain name to the CloudFormation outputs + */ + public addOutputs (domain: DomainConfig): void { + const service = this.serverless.service; + if (!service.provider.compiledCloudFormationTemplate.Outputs) { + service.provider.compiledCloudFormationTemplate.Outputs = {}; + } - // Defaults for REST and backwards compatibility - let distributionDomainNameOutputKey = "DistributionDomainName"; - let domainNameOutputKey = "DomainName"; - let hostedZoneIdOutputKey = "HostedZoneId"; + // Defaults for REST and backwards compatibility + let distributionDomainNameOutputKey = "DistributionDomainName"; + let domainNameOutputKey = "DomainName"; + let hostedZoneIdOutputKey = "HostedZoneId"; + + if (domain.apiType === Globals.apiTypes.http) { + distributionDomainNameOutputKey += "Http"; + domainNameOutputKey += "Http"; + hostedZoneIdOutputKey += "Http"; + } else if (domain.apiType === Globals.apiTypes.websocket) { + distributionDomainNameOutputKey += "Websocket"; + domainNameOutputKey += "Websocket"; + hostedZoneIdOutputKey += "Websocket"; + } - if (domain.apiType === Globals.apiTypes.http) { - distributionDomainNameOutputKey += "Http"; - domainNameOutputKey += "Http"; - hostedZoneIdOutputKey += "Http"; - } else if (domain.apiType === Globals.apiTypes.websocket) { - distributionDomainNameOutputKey += "Websocket"; - domainNameOutputKey += "Websocket"; - hostedZoneIdOutputKey += "Websocket"; + // for the CloudFormation stack we should use the `base` stage not the plugin custom stage + // Remove all special characters + const safeStage = Globals.getBaseStage().replace(/[^a-zA-Z\d]/g, ""); + service.provider.compiledCloudFormationTemplate.Outputs[domainNameOutputKey] = { + Value: domain.givenDomainName, + Export: { + Name: `sls-${service.service}-${safeStage}-${domainNameOutputKey}` } + }; - // for the CloudFormation stack we should use the `base` stage not the plugin custom stage - // Remove all special characters - const safeStage = Globals.getBaseStage().replace(/[^a-zA-Z\d]/g, ""); - service.provider.compiledCloudFormationTemplate.Outputs[domainNameOutputKey] = { - Value: domain.givenDomainName, + if (domain.domainInfo) { + service.provider.compiledCloudFormationTemplate.Outputs[distributionDomainNameOutputKey] = { + Value: domain.domainInfo.domainName, Export: { - Name: `sls-${service.service}-${safeStage}-${domainNameOutputKey}` + Name: `sls-${service.service}-${safeStage}-${distributionDomainNameOutputKey}` + } + }; + service.provider.compiledCloudFormationTemplate.Outputs[hostedZoneIdOutputKey] = { + Value: domain.domainInfo.hostedZoneId, + Export: { + Name: `sls-${service.service}-${safeStage}-${hostedZoneIdOutputKey}` } }; - - if (domain.domainInfo) { - service.provider.compiledCloudFormationTemplate.Outputs[distributionDomainNameOutputKey] = { - Value: domain.domainInfo.domainName, - Export: { - Name: `sls-${service.service}-${safeStage}-${distributionDomainNameOutputKey}` - } - }; - service.provider.compiledCloudFormationTemplate.Outputs[hostedZoneIdOutputKey] = { - Value: domain.domainInfo.hostedZoneId, - Export: { - Name: `sls-${service.service}-${safeStage}-${hostedZoneIdOutputKey}` - } - }; - } } + } } export = ServerlessCustomDomain; diff --git a/test/integration-tests/basic/basic.test.ts b/test/integration-tests/basic/basic.test.ts index af2ad2e5..d03630f8 100644 --- a/test/integration-tests/basic/basic.test.ts +++ b/test/integration-tests/basic/basic.test.ts @@ -168,4 +168,16 @@ describe("Integration Tests", function () { await utilities.destroyResources(testName); } }); + + it("Deploys multi base path for the same domain", async () => { + const testName = "deploy-idempotent2"; + const configFolder = `${CONFIGS_FOLDER}/${testName}`; + try { + await utilities.createTempDir(TEMP_DIR, configFolder); + await utilities.slsDeploy(TEMP_DIR); + await utilities.slsDeploy(TEMP_DIR); + } finally { + await utilities.destroyResources(testName); + } + }); }); diff --git a/test/integration-tests/basic/deploy-idempotent2/handler.js b/test/integration-tests/basic/deploy-idempotent2/handler.js new file mode 100644 index 00000000..1729e0e1 --- /dev/null +++ b/test/integration-tests/basic/deploy-idempotent2/handler.js @@ -0,0 +1,13 @@ +"use strict"; + +module.exports.helloWorld = (event, _context, callback) => { + const response = { + statusCode: 200, + body: JSON.stringify({ + message: "Go Serverless! Your function executed successfully!", + input: event, + }), + }; + + callback(null, response); +}; diff --git a/test/integration-tests/basic/deploy-idempotent2/serverless.yml b/test/integration-tests/basic/deploy-idempotent2/serverless.yml new file mode 100644 index 00000000..df6f94db --- /dev/null +++ b/test/integration-tests/basic/deploy-idempotent2/serverless.yml @@ -0,0 +1,35 @@ +# Deploying should be idempotent for the same domain and different paths +service: ${env:PLUGIN_IDENTIFIER}-deploy-idempotent2-${env:RANDOM_STRING} +provider: + name: aws + iam: + role: arn:aws:iam::${aws:accountId}:role/sls_domain_manager_lambda + runtime: nodejs16.x + region: us-west-2 + stage: test +functions: + helloWorld: + handler: handler.helloWorld + events: + - http: + path: hello-world + method: get + cors: true +plugins: + - serverless-domain-manager +custom: + customDomains: + - rest: + autoDomain: true + basePath: "path1" + domainName: ${env:PLUGIN_IDENTIFIER}-deploy-idempotent2-${env:RANDOM_STRING}.${env:TEST_DOMAIN} + allowPathMatching: true + - rest: + autoDomain: true + basePath: "path2" + domainName: ${env:PLUGIN_IDENTIFIER}-deploy-idempotent2-${env:RANDOM_STRING}.${env:TEST_DOMAIN} + allowPathMatching: true + +package: + patterns: + - '!node_modules/**' diff --git a/test/integration-tests/debug/pr-example/serverless.yml b/test/integration-tests/debug/pr-example/serverless.yml index 0ee2f9ff..d16aa98e 100644 --- a/test/integration-tests/debug/pr-example/serverless.yml +++ b/test/integration-tests/debug/pr-example/serverless.yml @@ -1,4 +1,4 @@ -service: ${env:PLUGIN_IDENTIFIER}-pr-example-${env:RANDOM_STRING} +service: ${env:PLUGIN_IDENTIFIER}-pr-example provider: name: aws iam: @@ -18,12 +18,17 @@ plugins: - serverless-domain-manager custom: - customDomain: - autoDomain: true - basePath: "" - domainName: ${env:PLUGIN_IDENTIFIER}-http-${env:RANDOM_STRING}.${env:TEST_DOMAIN} - createRoute53Record: true - endpointType: REGIONAL + customDomains: + - rest: + autoDomain: true + basePath: "path1" + domainName: ${env:PLUGIN_IDENTIFIER}-rest-debug.${env:TEST_DOMAIN} + allowPathMatching: true + - rest: + autoDomain: true + basePath: "path2" + domainName: ${env:PLUGIN_IDENTIFIER}-rest-debug.${env:TEST_DOMAIN} + allowPathMatching: true package: patterns: diff --git a/test/unit-tests/aws/api-gateway-v1-wrapper.test.ts b/test/unit-tests/aws/api-gateway-v1-wrapper.test.ts index 0826f4be..e6e3093e 100644 --- a/test/unit-tests/aws/api-gateway-v1-wrapper.test.ts +++ b/test/unit-tests/aws/api-gateway-v1-wrapper.test.ts @@ -449,7 +449,7 @@ describe("API Gateway V1 wrapper checks", () => { const commandCalls = APIGatewayMock.commandCalls(UpdateBasePathMappingCommand, expectedParams, true); expect(commandCalls.length).to.equal(1); - expect(consoleOutput[0]).to.contains("V1 - Updated API mapping from"); + expect(consoleOutput[0]).to.contains(`V1 - Updating API mapping from '${dc.apiMapping.basePath}'`); }); it("update base path mapping failure", async () => { diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index 398ec0fc..01c4b0b8 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -211,16 +211,6 @@ describe("Custom Domain Plugin", () => { expect(plugin.domains.length).to.equal(1); }); - it("allowPathMatching", () => { - const domainOptions = getDomainConfig({ allowPathMatching: true }); - const plugin = constructPlugin(domainOptions); - - plugin.initializeVariables(); - plugin.validateDomainConfigs(); - - expect(consoleOutput[0]).to.contains("This should only be used when migrating a path to a different API type. e.g. REST to HTTP."); - }); - it("Should enable the plugin by default", () => { const plugin = constructPlugin(getDomainConfig({}));