├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apidocs
└── server.js
├── app.py
├── cdk.json
├── checkov.yaml
├── docs
└── assets
│ ├── api-gateway-routes.png
│ ├── cdk-deploy-screenshot.png
│ ├── cdk-destroy-screenshot.png
│ ├── cdk-stacks-screenshot.png
│ ├── solution-architecture.drawio
│ ├── solution-architecture.png
│ └── swagger-ui-screenshot.png
├── examples
├── test_greeting_api.sh
└── test_ping_api.sh
├── package-lock.json
├── package.json
├── requirements.txt
├── setup.py
├── source.bat
├── stacks
├── __init__.py
├── apigateway_dynamic_publish.py
└── resources
│ ├── api_creation
│ ├── api_creator.py
│ ├── api_definition.yaml
│ └── requirements.txt
│ └── api_integrations
│ ├── greeting.py
│ └── ping.py
├── tests
├── __init__.py
└── test_apigateway_dynamic_publish.py
└── view_api_docs.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 |
132 | # VSCode extension
133 | .vscode/
134 | /.favorites.json
135 |
136 | # TypeScript incremental build states
137 | *.tsbuildinfo
138 |
139 | # Local state files & OS specifics
140 | .DS_Store
141 | node_modules/
142 | lerna-debug.log
143 | dist/
144 | pack/
145 | .BUILD_COMPLETED
146 | .local-npm/
147 | .tools/
148 | coverage/
149 | .nyc_output
150 | .LAST_BUILD
151 | *.sw[a-z]
152 | *~
153 | .idea
154 |
155 | # We don't want tsconfig at the root
156 | /tsconfig.json
157 |
158 | # CDK Context & Staging files
159 | cdk.context.json
160 | .cdk.staging/
161 | cdk.out/
162 |
163 |
164 | ########################
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # api-gateway-dynamic-publish
2 |
3 | CDK project that leverages an OpenAPI definition to define, document and create an Amazon API Gateway deployment. At deploy time, a [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) is leveraged to dynamically substitute the Lambda integration function ARNs into the OpenAPI definition file as well as publishing the updated file to an S3 bucket for documentation viewing.
4 |
5 | ----
6 |
7 | * [Project overview](#project-overview)
8 | * [Solution overview](#solution-overview)
9 | * [Key points of the solution](#key-points-of-the-solution)
10 | * [Deploying the solution](#deploying-the-solution)
11 | * [Testing the Amazon API Gateway endpoints](#testing-the-amazon-api-gateway-endpoints)
12 | * [Ping endpoint](#ping-endpoint)
13 | * [Greeting endpoint](#greeting-endpoint)
14 | * [Viewing the API documentation](#viewing-the-api-documentation)
15 | * [Clean-up the solution](#clean-up-the-solution)
16 | * [Conclusion](#conclusion)
17 | * [Executing unit tests](#executing-unit-tests)
18 | * [Executing static code analysis tool](#executing-static-code-analysis-tool)
19 | * [Security](#security)
20 | * [License](#license)
21 |
22 | ## Project overview
23 |
24 | [Amazon API Gateway](https://aws.amazon.com/api-gateway/) is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. APIs act as the "front door" for applications to access data, business logic, or functionality from your backend services. Using API Gateway, you can create RESTful APIs and WebSocket APIs that enable real-time two-way communication applications. API Gateway supports containerized and serverless workloads, as well as web applications.
25 |
26 | The [OpenAPI Specification (OAS)](https://swagger.io/specification/) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. An OpenAPI definition can then be used by documentation generation tools to display the API.
27 |
28 | This blog post will describe how an OpenAPI definition can be used to define, document and create an Amazon API Gateway deployment from a single definition file. At deploy time, a [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) is leveraged to dynamically substitute the Lambda integration function ARNs into the OpenAPI definition file as well as publishing the updated file to an S3 bucket for documentation viewing.
29 |
30 | ## Solution overview
31 |
32 | The solution architecture discussed in this post is presented below:
33 |
34 | 
35 |
36 | 1. CDK is used to synthesize a CloudFormation template.
37 | 2. The generated CloudFormation template includes the definition of a custom resource. The custom resource is implemented via a Lambda function which will dynamically substitute the function ARNs of the Amazon API Gateway Lambda integrations into the OpenAPI definition file.
38 | 3. Once the custom resource has completed the substitution processes, the resulting OpenAPI definition file is used to create the Amazon API Gateway. The same OpenAPI definition file is then published to an S3 bucket to allow for documentation viewing.
39 | 4. Upon successful deployment of the CloudFormation stack, end users can invoke the Amazon API Gateway. They can also view the API documentation via the [Swagger UI](https://swagger.io/tools/swagger-ui/).
40 |
41 | ## Key points of the solution
42 |
43 | The relevant section of the CDK [stacks/apigateway_dynamic_publish.py](stacks/apigateway_dynamic_publish.py) stack, in which the Custom Resource and Lambda are defined, is shown below:
44 |
45 | ```python
46 | # Create a role for the api creator lambda function
47 | apicreator_lambda_role = iam.Role(
48 | scope=self,
49 | id="ApiCreatorLambdaRole",
50 | assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
51 | managed_policies=[
52 | iam.ManagedPolicy.from_aws_managed_policy_name(
53 | "service-role/AWSLambdaBasicExecutionRole"
54 | )
55 | ]
56 | )
57 | apicreator_lambda_role.add_to_policy(
58 | iam.PolicyStatement(
59 | effect=iam.Effect.ALLOW,
60 | resources=[
61 | "arn:aws:apigateway:*::/apis/*",
62 | "arn:aws:apigateway:*::/apis"
63 | ],
64 | actions=[
65 | "apigateway:DELETE",
66 | "apigateway:PUT",
67 | "apigateway:PATCH",
68 | "apigateway:POST",
69 | "apigateway:GET"
70 | ]
71 | )
72 | )
73 | apicreator_lambda_role.add_to_policy(
74 | iam.PolicyStatement(
75 | effect=iam.Effect.ALLOW,
76 | resources=["*"],
77 | actions=[
78 | "logs:*"
79 | ]
80 | )
81 | )
82 | api_documentation_bucket.grant_read_write(apicreator_lambda_role)
83 | apicreator_lambda = aws_lambda.Function(
84 | scope=self,
85 | id="ApiCreatorLambda",
86 | code=aws_lambda.Code.from_asset(
87 | f"{os.path.dirname(__file__)}/resources/api_creation",
88 | bundling=BundlingOptions(
89 | image=aws_lambda.Runtime.PYTHON_3_9.bundling_image,
90 | command=[
91 | "bash", "-c",
92 | "pip install --no-cache -r requirements.txt -t /asset-output && cp -au . /asset-output"
93 | ],
94 | ),
95 | ),
96 | handler="api_creator.lambda_handler",
97 | role=apicreator_lambda_role,
98 | runtime=aws_lambda.Runtime.PYTHON_3_9,
99 | timeout=Duration.minutes(5)
100 | )
101 | # Provider that invokes the api creator lambda function
102 | apicreator_provider = custom_resources.Provider(
103 | self,
104 | 'ApiCreatorCustomResourceProvider',
105 | on_event_handler=apicreator_lambda
106 | )
107 | # The custom resource that uses the api creator provider to supply values
108 | apicreator_custom_resource = CustomResource(
109 | self,
110 | 'ApiCreatorCustomResource',
111 | service_token=apicreator_provider.service_token,
112 | properties={
113 | 'ApiGatewayAccessLogsLogGroupArn': api_gateway_access_log_group.log_group_arn,
114 | 'ApiIntegrationPingLambda': api_gateway_ping_lambda.function_arn,
115 | 'ApiIntegrationGreetingLambda': api_gateway_greeting_lambda.function_arn,
116 | 'ApiDocumentationBucketName': api_documentation_bucket.bucket_name,
117 | 'ApiDocumentationBucketUrl': api_documentation_bucket.bucket_website_url,
118 | 'ApiName': f"{config['api']['apiName']}",
119 | 'ApiStageName': config['api']['apiStageName'],
120 | 'ThrottlingBurstLimit': config['api']['throttlingBurstLimit'],
121 | 'ThrottlingRateLimit': config['api']['throttlingRateLimit']
122 | }
123 | ))
124 |
125 | apigateway_id = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiId')
126 | apigateway_endpoint = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiEndpoint')
127 | apigateway_stagename = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiStageName')
128 | ```
129 |
130 | -----
131 |
132 | The [stacks/resources/api_creation/api_creator.py](stacks/resources/api_creation/api_creator.py) Lambda function, called by the Custom Resource, is shown below:
133 |
134 | ```python
135 | #!/usr/bin/env python
136 |
137 | """
138 | api_creator.py:
139 | Cloudformation custom resource lambda handler which performs the following tasks:
140 | * injects lambda functions arns (created during CDK deployment) into the
141 | OpenAPI 3 spec file (api_definition.yaml)
142 | * deploys or updates the API Gateway stage using the OpenAPI 3 spec file (api_definition.yaml)
143 | * deletes the API Gateway stage (if the Cloudformation operation is delete)
144 | """
145 |
146 | import json
147 | import logging
148 | import os
149 |
150 | import boto3
151 | import yaml
152 |
153 | # set logging
154 | logger = logging.getLogger()
155 | logger.setLevel(logging.DEBUG)
156 |
157 | # environment variables
158 | aws_region = os.environ['AWS_REGION']
159 |
160 | # boto3 clients
161 | apigateway_client = boto3.client('apigatewayv2')
162 | s3_client = boto3.client('s3')
163 |
164 | def replace_placeholders(template_file: str, substitutions: dict) -> str:
165 | import re
166 |
167 | def from_dict(dct):
168 | def lookup(match):
169 | key = match.group(1)
170 | return dct.get(key, f'<{key} not found>')
171 | return lookup
172 |
173 | with open (template_file, "r") as template_file:
174 | template_data = template_file.read()
175 |
176 | # perform the subsitutions, looking for placeholders @@PLACEHOLDER@@
177 | api_template = re.sub('@@(.*?)@@', from_dict(substitutions), template_data)
178 |
179 | return api_template
180 |
181 |
182 | def get_api_by_name(api_name: str) -> str:
183 | get_apis = apigateway_client.get_apis()
184 | for api in get_apis['Items']:
185 | if api['Name'] == api_name:
186 | return api['ApiId']
187 |
188 | return None
189 |
190 |
191 | def create_api(api_template: str) -> str:
192 | api_response = apigateway_client.import_api(
193 | Body=api_template,
194 | FailOnWarnings=True
195 | )
196 |
197 | return api_response['ApiEndpoint'], api_response['ApiId']
198 |
199 |
200 | def update_api(api_template: str, api_name: str) -> str:
201 |
202 | api_id = get_api_by_name(api_name)
203 |
204 | if api_id is not None:
205 | api_response = apigateway_client.reimport_api(
206 | ApiId=api_id,
207 | Body=api_template,
208 | FailOnWarnings=True
209 | )
210 | return api_response['ApiEndpoint'], api_response['ApiId']
211 |
212 |
213 | def delete_api(api_name: str) -> None:
214 | if get_api_by_name(api_name) is not None:
215 | apigateway_client.delete_api(
216 | ApiId=get_api_by_name(api_name)
217 | )
218 |
219 |
220 | def deploy_api(
221 | api_id: str,
222 | api_stage_name: str,
223 | api_access_logs_arn: str,
224 | throttling_burst_limit: int,
225 | throttling_rate_limit: int
226 | ) -> None:
227 | apigateway_client.create_stage(
228 | AccessLogSettings={
229 | 'DestinationArn': api_access_logs_arn,
230 | 'Format': '$context.identity.sourceIp - - [$context.requestTime] "$context.httpMethod $context.routeKey $context.protocol" $context.status $context.responseLength $context.requestId $context.integrationErrorMessage'
231 | },
232 | ApiId=api_id,
233 | StageName=api_stage_name,
234 | AutoDeploy=True,
235 | DefaultRouteSettings={
236 | 'DetailedMetricsEnabled': True,
237 | 'ThrottlingBurstLimit':throttling_burst_limit,
238 | 'ThrottlingRateLimit': throttling_rate_limit
239 | }
240 | )
241 |
242 |
243 | def delete_api_deployment(api_id: str, api_stage_name: str) -> None:
244 | try:
245 | apigateway_client.get_stage(
246 | ApiId=api_id,
247 | StageName=api_stage_name
248 | )
249 |
250 | apigateway_client.delete_stage(
251 | ApiId=api_id,
252 | StageName=api_stage_name
253 | )
254 | except apigateway_client.exceptions.NotFoundException as e:
255 | logger.error(f"Stage name: {api_stage_name} for api id: {api_id} was not found during stage deletion. This is an expected error condition and is handled in code.")
256 | except Exception as e:
257 | raise ValueError(f"Unexpected error encountered during api deployment deletion: {str(e)}")
258 |
259 |
260 | def publish_api_documentation(bucket_name: str, api_definition: str) -> None:
261 |
262 | api_definition_json=json.dumps(yaml.safe_load(api_definition))
263 |
264 | with open("/tmp/swagger.json", "w") as swagger_file:
265 | swagger_file.write(api_definition_json)
266 |
267 | # Upload the file
268 | try:
269 |
270 | s3_client.upload_file("/tmp/swagger.json", bucket_name, "swagger.json")
271 |
272 | except Exception as e:
273 | logging.error(str(e))
274 | raise ValueError(str(e))
275 |
276 |
277 | def lambda_handler(event, context):
278 |
279 | # print the event details
280 | logger.debug(json.dumps(event, indent=2))
281 |
282 | props = event['ResourceProperties']
283 | api_gateway_access_log_group_arn = props['ApiGatewayAccessLogsLogGroupArn']
284 | api_integration_ping_lambda = props['ApiIntegrationPingLambda']
285 | api_integration_greetings_lambda = props['ApiIntegrationGreetingLambda']
286 | api_name = props['ApiName']
287 | api_stage_name = props['ApiStageName']
288 | api_documentation_bucket_name = props['ApiDocumentationBucketName']
289 | throttling_burst_limit = int(props['ThrottlingBurstLimit'])
290 | throttling_rate_limit = int(props['ThrottlingRateLimit'])
291 |
292 | lambda_substitutions = {
293 | "API_NAME": api_name,
294 | "API_INTEGRATION_PING_LAMBDA": f"arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{api_integration_ping_lambda}/invocations",
295 | "API_INTEGRATION_GREETING_LAMBDA": f"arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{api_integration_greetings_lambda}/invocations"
296 | }
297 |
298 | api_template = replace_placeholders("api_definition.yaml", lambda_substitutions)
299 |
300 | if event['RequestType'] != 'Delete':
301 |
302 | if get_api_by_name(api_name) is None:
303 |
304 | logger.debug("Creating API")
305 |
306 | api_endpoint, api_id = create_api(api_template)
307 |
308 | deploy_api(api_id, api_stage_name, api_gateway_access_log_group_arn, throttling_burst_limit, throttling_rate_limit)
309 |
310 | publish_api_documentation(api_documentation_bucket_name, api_template)
311 |
312 | output = {
313 | 'PhysicalResourceId': f"generated-api",
314 | 'Data': {
315 | 'ApiEndpoint': api_endpoint,
316 | 'ApiId': api_id,
317 | 'ApiStageName': api_stage_name
318 | }
319 | }
320 |
321 | return output
322 |
323 | else:
324 |
325 | logger.debug("Updating API")
326 |
327 | api_endpoint, api_id = update_api(api_template, api_name)
328 |
329 | # delete and redeploy the stage after updating the api definition
330 | delete_api_deployment(api_id, api_stage_name)
331 | deploy_api(api_id, api_stage_name, api_gateway_access_log_group_arn, throttling_burst_limit, throttling_rate_limit)
332 |
333 | publish_api_documentation(api_documentation_bucket_name, api_template)
334 |
335 | output = {
336 | 'PhysicalResourceId': f"generated-api",
337 | 'Data': {
338 | 'ApiEndpoint': api_endpoint,
339 | 'ApiId': api_id,
340 | 'ApiStageName': api_stage_name
341 | }
342 | }
343 |
344 | return output
345 |
346 | if event['RequestType'] == 'Delete':
347 |
348 | logger.debug("Deleting API")
349 |
350 | if get_api_by_name(api_name) is not None:
351 | delete_api(api_name)
352 |
353 | output = {
354 | 'PhysicalResourceId': f"generated-api",
355 | 'Data': {
356 | 'ApiEndpoint': "Deleted",
357 | 'ApiId': "Deleted",
358 | 'ApiStageName': "Deleted"
359 | }
360 | }
361 | logger.info(output)
362 |
363 | return output
364 | ```
365 |
366 | -----
367 |
368 | The OpenAPI definition file, [stacks/resources/api_creation/api_definition.yaml](stacks/resources/api_creation/api_definition.yaml), is shown below. Note the presence of the dynamic variables represented as `@@VARIABLE_NAME@@` which will be replaced by the custom resource.
369 |
370 | ```yaml
371 | openapi: "3.0.0"
372 | info:
373 | title: @@API_NAME@@
374 | version: "v1.0"
375 | x-amazon-apigateway-request-validators:
376 | all:
377 | validateRequestBody: true
378 | validateRequestParameters: true
379 | params-only:
380 | validateRequestBody: false
381 | validateRequestParameters: true
382 | x-amazon-apigateway-request-validator: all
383 | paths:
384 | /ping:
385 | get:
386 | summary: "Simulates an API Ping"
387 | description: |
388 | ## Simulates an API Ping
389 |
390 | The purpose of this endpoint is to simulate a Ping request and respond with a Pong answer.
391 | operationId: "pingIntegration"
392 | x-amazon-apigateway-request-validator: all
393 | responses:
394 | 200:
395 | description: "OK"
396 | content:
397 | application/json:
398 | schema:
399 | type: array
400 | items:
401 | $ref: "#/components/schemas/PingResponse"
402 | 500:
403 | description: "Internal Server Error"
404 | x-amazon-apigateway-integration:
405 | uri: @@API_INTEGRATION_PING_LAMBDA@@
406 | payloadFormatVersion: "2.0"
407 | httpMethod: "POST"
408 | type: "aws_proxy"
409 | connectionType: "INTERNET"
410 | /greeting:
411 | get:
412 | summary: "Get a greeting message"
413 | description: |
414 | ## Get a greeting message
415 |
416 | The purpose of this endpoint is send a greeting string and receive a greeting message.
417 | operationId: "greetingIntegration"
418 | x-amazon-apigateway-request-validator: all
419 | parameters:
420 | - in: query
421 | name: greeting
422 | schema:
423 | type: string
424 | description: |
425 | A greeting string which the API will combine to form a greeting message
426 | responses:
427 | 200:
428 | description: "OK"
429 | content:
430 | application/json:
431 | schema:
432 | type: array
433 | items:
434 | $ref: "#/components/schemas/GreetingResponse"
435 | 500:
436 | description: "Internal Server Error"
437 | x-amazon-apigateway-integration:
438 | uri: @@API_INTEGRATION_GREETING_LAMBDA@@
439 | payloadFormatVersion: "2.0"
440 | httpMethod: "POST"
441 | type: "aws_proxy"
442 | connectionType: "INTERNET"
443 | components:
444 | schemas:
445 | PingResponse:
446 | type: object
447 | properties:
448 | ping:
449 | type: string
450 | description: |
451 | Response to the ping request.
452 | GreetingResponse:
453 | type: object
454 | properties:
455 | greeting:
456 | type: string
457 | description: |
458 | The greeting response which concatenates the incoming greeting to form a greeting message.
459 | ```
460 |
461 | ## Deploying the solution
462 |
463 | The solution code uses the Python flavour of the AWS CDK ([Cloud Development Kit](https://aws.amazon.com/cdk/)). In order to execute the solution code, please ensure that you have fulfilled the [AWS CDK Prerequisites for Python](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-python.html).
464 |
465 | Additionally, the project assumes:
466 |
467 | * configuration of [AWS CLI Environment Variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html).
468 | * the availability of a `bash` (or compatible) shell environment.
469 | * a [Docker](https://www.docker.com/) installation.
470 |
471 | The solution code requires that the AWS account is [bootstrapped](https://docs.aws.amazon.com/de_de/cdk/latest/guide/bootstrapping.html) in order to allow the deployment of the solution’s CDK stack.
472 |
473 | ```bash
474 | # navigate to project directory
475 | cd api-gateway-dynamic-publish
476 |
477 | # install and activate a Python Virtual Environment
478 | python3 -m venv .venv
479 | source .venv/bin/activate
480 |
481 | # install dependant libraries
482 | python -m pip install -r requirements.txt
483 |
484 | # bootstrap the account to permit CDK deployments
485 | cdk bootstrap
486 | ```
487 |
488 | Upon successful completion of `cdk bootstrap`, the solution is ready to be deployed.
489 |
490 | The CDK stack can be deployed with the command below.
491 |
492 | ```bash
493 | cdk deploy
494 | ```
495 |
496 | 
497 |
498 | Following a successful deployment, verify that two new stacks have been created.
499 |
500 | * `CDKToolkit`
501 | * `ApiGatewayDynamicPublish`
502 |
503 | Log into the AWS Console → navigate to the CloudFormation console:
504 | [Image: cdk-stacks-screenshot.png]Verify the successful deployment of the Amazon API Gateway.
505 |
506 | 1. Log into the AWS Console → navigate to the API Gateway console.
507 | 2. Click on the API with name `apigateway-dynamic-publish` to open the detailed API view.
508 | 3. Click on `Develop` → `Integrations` and verify the `/greeting` and `/ping` routes.
509 |
510 | 
511 |
512 | The CDK stack has successfully deployed the Amazon API Gateway according to the specifications described in the OpenAPI definition file, [stacks/resources/api_creation/api_definition.yaml](stacks/resources/api_creation/api_definition.yaml).
513 |
514 | ## Testing the Amazon API Gateway endpoints
515 |
516 | The project includes 2 test scripts that can be executed to test the `/ping` and `/greeting` API endpoints respectively.
517 |
518 | ### Ping endpoint
519 |
520 | Test the `/ping` API endpoint with the command below:
521 |
522 | ```bash
523 | bash examples/test_ping_api.sh
524 | ```
525 |
526 | An example of a successful response is shown below:
527 |
528 | ```bash
529 | Testing GET https://xxxxxxxx.execute-api.xx-xxxx-x.amazonaws.com/dev/ping
530 |
531 | { "ping": "Pong" }
532 | ```
533 |
534 | ### Greeting endpoint
535 |
536 | Test the `/greeting` API endpoint with the command below:
537 |
538 | ```bash
539 | bash examples/test_greeting_api.sh
540 | ```
541 |
542 | An example of a successful response is shown below:
543 |
544 | ```bash
545 | Testing GET https://xxxxxxxx.execute-api.xx-xxxx-x.amazonaws.com/dev/greeting?greeting=world
546 |
547 | { "greeting": "Hello world" }
548 | ```
549 |
550 | ## Viewing the API documentation
551 |
552 | During the project deployment, the OpenAPI definition file [stacks/resources/api_creation/api_definition.yaml](stacks/resources/api_creation/api_definition.yaml), is uploaded to an S3 bucket where it can be consumed to visualize the API documentation via [Swagger UI](https://github.com/swagger-api/swagger-ui).
553 |
554 | To view API documentation in the OpenAPI format, it is necessary to download Swagger UI which is a third party, open source project licensed under the Apache License 2.0.
555 |
556 | More information about Swagger UI can be found on the project's GitHub page; https://github.com/swagger-api/swagger-ui.
557 |
558 | The command below can be used to launch the API documentation viewer. The command will ask you for permission to download Swagger UI.
559 |
560 | ```bash
561 | bash apidocs/view_api_docs.sh
562 | ```
563 |
564 | The command starts a [Node.js](https://nodejs.org/en/) server listening at [http://localhost:12345](http://localhost:12345/)
565 |
566 | Open [http://localhost:12345](http://localhost:12345/) in your preferred browser to view the API documentation.
567 |
568 | 
569 |
570 | ## Clean-up the solution
571 |
572 | Solution clean-up is a 2 step process:
573 |
574 | 1. Destroy the CDK stack.
575 | 2. Delete the *CDKToolkit* stack from CloudFormation.
576 |
577 | Delete the stack deployed by CDK with the command below:
578 |
579 | ```bash
580 | cdk destroy
581 | ```
582 |
583 | 
584 |
585 | Delete the CDKToolkit CloudFormation stack.
586 |
587 | 1. Log into the AWS Console → navigate to the *CloudFormation* console.
588 | 2. Navigate to *Stacks*.
589 | 3. Select the **CDKToolkit**.
590 | 4. Click the *Delete* button.
591 |
592 | ## Conclusion
593 |
594 | In this blog post we have a seen how an OpenAPI definition file can be leveraged to define, document and create an Amazon API Gateway.
595 |
596 | A challenge that arises when defining, documenting and creating an Amazon API Gateway via an Infrastructure as Code approach is that the function ARNs of the Amazon API Gateway Lambda integrations must be added to the OpenAPI definition file prior to API Gateway creation. The function ARNs are not generated until deploy time.
597 |
598 | This solution provides a [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) which overcomes this challenge by dynamically substituting the Lambda integration function ARNs into the OpenAPI definition file allowing for the creation of the Amazon API Gateway and the publishing of the API documentation to an S3 bucket.
599 |
600 | This blog post focuses on an Amazon API Gateway specific use case, however the Custom Resource pattern is very flexible and can be used to extend and enhance the functionality of CloudFormation templates and CDK stacks across a variety of use case scenarios.
601 |
602 | # Executing unit tests
603 |
604 | Unit tests for the project can be executed via the command below:
605 |
606 | ```bash
607 | python3 -m venv .venv
608 | source .venv/bin/activate
609 | cdk synth && python -m pytest
610 | ```
611 |
612 | # Executing static code analysis tool
613 |
614 | The solution includes [Checkov](https://github.com/bridgecrewio/checkov) which is a static code analysis tool for infrastructure as code (IaC).
615 |
616 | The static code analysis tool for the project can be executed via the commands below:
617 |
618 | ```bash
619 | python3 -m venv .venv
620 | source .venv/bin/activate
621 | rm -fr cdk.out && cdk synth && checkov --config-file checkov.yaml
622 | ```
623 |
624 | **NOTE:** The Checkov tool has been configured to skip certain checks.
625 |
626 | The Checkov configuration file, [checkov.yaml](checkov.yaml), contains a section named `skip-check`.
627 |
628 | ```
629 | skip-check:
630 | - CKV_AWS_7 # Ensure rotation for customer created CMKs is enabled
631 | - CKV_AWS_18 # Ensure the S3 bucket has access logging enabled
632 | - CKV_AWS_19 # Ensure the S3 bucket has server-side-encryption enabled
633 | - CKV_AWS_20 # Ensure the S3 bucket does not allow READ permissions to everyone
634 | - CKV_AWS_21 # Ensure the S3 bucket has versioning enabled
635 | - CKV_AWS_33 # Ensure KMS key policy does not contain wildcard (*) principal
636 | - CKV_AWS_40 # Ensure IAM policies are attached only to groups or roles (Reducing access management complexity may in-turn reduce opportunity for a principal to inadvertently receive or retain excessive privileges.)
637 | - CKV_AWS_45 # Ensure no hard-coded secrets exist in lambda environment
638 | - CKV_AWS_53 # Ensure S3 bucket has block public ACLS enabled
639 | - CKV_AWS_54 # Ensure S3 bucket has block public policy enabled
640 | - CKV_AWS_55 # Ensure S3 bucket has ignore public ACLs enabled
641 | - CKV_AWS_56 # Ensure S3 bucket has 'restrict_public_bucket' enabled
642 | - CKV_AWS_57 # Ensure the S3 bucket does not allow WRITE permissions to everyone
643 | - CKV_AWS_60 # Ensure IAM role allows only specific services or principals to assume it
644 | - CKV_AWS_61 # Ensure IAM role allows only specific principals in account to assume it
645 | - CKV_AWS_62 # Ensure no IAM policies that allow full "*-*" administrative privileges are not created
646 | - CKV_AWS_63 # Ensure no IAM policies documents allow "*" as a statement's actions
647 | - CKV_AWS_66 # Ensure that CloudWatch Log Group specifies retention days
648 | - CKV_AWS_107 # Ensure IAM policies does not allow credentials exposure
649 | - CKV_AWS_108 # Ensure IAM policies does not allow data exfiltration
650 | - CKV_AWS_109 # Ensure IAM policies does not allow permissions management without constraints
651 | - CKV_AWS_110 # Ensure IAM policies does not allow privilege escalation
652 | - CKV_AWS_111 # Ensure IAM policies does not allow write access without constraints
653 | - CKV_AWS_115 # Ensure that AWS Lambda function is configured for function-level concurrent execution limit
654 | - CKV_AWS_116 # Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ)
655 | - CKV_AWS_117 # Ensure that AWS Lambda function is configured inside a VPC
656 | - CKV_AWS_119 # Ensure DynamoDB Tables are encrypted using a KMS Customer Managed CMK
657 | - CKV_AWS_158 # Ensure that CloudWatch Log Group is encrypted by KMS
658 | - CKV_AWS_173 # Check encryption settings for Lambda environmental variable
659 | ```
660 |
661 | These checks represent best practices in AWS and should be enabled (or at the very least the security risk of not enabling the checks should be accepted and understood) for production systems.
662 |
663 | In the context of this solution, these specific checks have not been remediated in order to focus on the core elements of the solution.
664 |
665 | # Security
666 |
667 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
668 |
669 | # License
670 |
671 | This library is licensed under the MIT-0 License. See the LICENSE file.
--------------------------------------------------------------------------------
/apidocs/server.js:
--------------------------------------------------------------------------------
1 | var express = require("express");
2 |
3 | var app = express();
4 |
5 | app.use(express.static('swagger-ui'));
6 |
7 | //make way for some custom css, js and images
8 | app.use('/', express.static(__dirname + '/swagger-ui'));
9 |
10 | var server = app.listen(12345, function(){
11 | var port = server.address().port;
12 | console.log("Server started at http://localhost:%s", port);
13 | });
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import aws_cdk
4 |
5 | from stacks.apigateway_dynamic_publish import ApiGatewayDynamicPublishStack
6 |
7 | app = aws_cdk.App()
8 | ApiGatewayDynamicPublishStack(app, "ApiGatewayDynamicPublish")
9 |
10 | app.synth()
11 |
--------------------------------------------------------------------------------
/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "python3 app.py",
3 | "context": {
4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
5 | "aws-cdk:enableDiffNoFail": "true",
6 | "@aws-cdk/core:stackRelativeExports": "true",
7 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
8 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true,
9 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
10 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
11 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true,
12 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
13 | "api": {
14 | "apiName": "apigateway-dynamic-publish",
15 | "apiStageName": "dev",
16 | "throttlingBurstLimit": 500,
17 | "throttlingRateLimit":100
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/checkov.yaml:
--------------------------------------------------------------------------------
1 | branch: main
2 | download-external-modules: true
3 | evaluate-variables: true
4 | external-modules-download-path: .external_modules
5 | framework: cloudformation
6 | output: cli
7 | directory:
8 | - cdk.out
9 | skip-check:
10 | - CKV_AWS_7 # Ensure rotation for customer created CMKs is enabled
11 | - CKV_AWS_18 # Ensure the S3 bucket has access logging enabled
12 | - CKV_AWS_19 # Ensure the S3 bucket has server-side-encryption enabled
13 | - CKV_AWS_20 # Ensure the S3 bucket does not allow READ permissions to everyone
14 | - CKV_AWS_21 # Ensure the S3 bucket has versioning enabled
15 | - CKV_AWS_33 # Ensure KMS key policy does not contain wildcard (*) principal
16 | - CKV_AWS_40 # Ensure IAM policies are attached only to groups or roles (Reducing access management complexity may in-turn reduce opportunity for a principal to inadvertently receive or retain excessive privileges.)
17 | - CKV_AWS_45 # Ensure no hard-coded secrets exist in lambda environment
18 | - CKV_AWS_53 # Ensure S3 bucket has block public ACLS enabled
19 | - CKV_AWS_54 # Ensure S3 bucket has block public policy enabled
20 | - CKV_AWS_55 # Ensure S3 bucket has ignore public ACLs enabled
21 | - CKV_AWS_56 # Ensure S3 bucket has 'restrict_public_bucket' enabled
22 | - CKV_AWS_57 # Ensure the S3 bucket does not allow WRITE permissions to everyone
23 | - CKV_AWS_60 # Ensure IAM role allows only specific services or principals to assume it
24 | - CKV_AWS_61 # Ensure IAM role allows only specific principals in account to assume it
25 | - CKV_AWS_62 # Ensure no IAM policies that allow full "*-*" administrative privileges are not created
26 | - CKV_AWS_63 # Ensure no IAM policies documents allow "*" as a statement's actions
27 | - CKV_AWS_66 # Ensure that CloudWatch Log Group specifies retention days
28 | - CKV_AWS_107 # Ensure IAM policies does not allow credentials exposure
29 | - CKV_AWS_108 # Ensure IAM policies does not allow data exfiltration
30 | - CKV_AWS_109 # Ensure IAM policies does not allow permissions management without constraints
31 | - CKV_AWS_110 # Ensure IAM policies does not allow privilege escalation
32 | - CKV_AWS_111 # Ensure IAM policies does not allow write access without constraints
33 | - CKV_AWS_115 # Ensure that AWS Lambda function is configured for function-level concurrent execution limit
34 | - CKV_AWS_116 # Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ)
35 | - CKV_AWS_117 # Ensure that AWS Lambda function is configured inside a VPC
36 | - CKV_AWS_119 # Ensure DynamoDB Tables are encrypted using a KMS Customer Managed CMK
37 | - CKV_AWS_158 # Ensure that CloudWatch Log Group is encrypted by KMS
38 | - CKV_AWS_173 # Check encryption settings for Lambda environmental variable
--------------------------------------------------------------------------------
/docs/assets/api-gateway-routes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/api-gateway-routes.png
--------------------------------------------------------------------------------
/docs/assets/cdk-deploy-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/cdk-deploy-screenshot.png
--------------------------------------------------------------------------------
/docs/assets/cdk-destroy-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/cdk-destroy-screenshot.png
--------------------------------------------------------------------------------
/docs/assets/cdk-stacks-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/cdk-stacks-screenshot.png
--------------------------------------------------------------------------------
/docs/assets/solution-architecture.drawio:
--------------------------------------------------------------------------------
1 | 7VtbU9s4FP41mek+kLEs3/JIEmDZZadMGbbsEyNixfHiWKntkIRfv5IlO9YlIS1xMV1aBqxjW5dz9J3zSUfuwdF8fZGhxewvEuKkZ1vhugfHPdseOB79zQQbLgB+4HBJlMWhkG0FN/EzFkJLSJdxiHPpwYKQpIgXsnBC0hRPCkmGsoys5MemJJFbXaAIa4KbCUp06dc4LGZcGtj+Vv47jqNZ1TLwBvzOHFUPi5HkMxSSVUMEz3pwlBFS8Kv5eoQTprxKL/y98x13645lOC0OecF7zu5uH2J4+fnLN/w4fL68SJ0TUcsTSpZiwKdfb6hglJBlKPpdbCplLEicFqVC3SH9oe2NrJ5L74xYqW+7ikAt+7IA6CVWhyxQy74sAGr1QGkfqB1sCLSSVL2ltG81Okh/4JAsiyRO8aieehYVRhkKY2qSEUlIRmUpSan2hrNintASoJerWVzgmwWaMK2uKGyobErSQkx+YFdloXhWK508C3Y9X0cMZ320yp1+lJHlomzykk5/4917enk/Yca8R0nBKioy8oirzvVsSP+fswkznMZJonT6CWdFTLFwmsQRq78grDkkSgmeljXSkcRpdFWWxtASvTc1EaJ8hkMxJH3+VpORtorXDZGYzxeYzHGRbegj4i4U0KqciyfKqwZSLSGbNVDqVi8i4R2iuuotgOiFwND34MnSQIND6lBEkWTFjEQkRcnZVjqkhkrDWi/bZ64I03c5Z/7FRbEREwQtCyLPqJ2qzMkym4h+/AGof03uHu/As/sY5bcna//qxBYuFWURLvaMC/Ln2Fj2GibDCSriJ9l5mrQsXr1mHmVr0NqCwqJuoBiKD0i8pdiq7sYrzDfQ/OEkfGSK3KR0RplMe4UeaMiTzFEBZELNgTMDjuZxGHLL4zx+Rg81zGUHOzaadu+807BTR0bRihR8TJg6sfoAejKueOlHbVs9QqbTHLdjNmA0TRdQtw9NL6LOOTbqXuXbKiLTAMcYLxKyKTWPJo9dxseu2PIj+IC+578OElUPHPmN9hACNdOVDO+cZJSnxpQ8qKYzchspfldGKw18TfK4rAeOH0hRkPmL7KE2fmNiGFgO7JfsZVp3VCYqlF347mBUsosdzOsIPMMOXDksQdh3Narhe5WwyTUGsC2qYb8vp+cc6PTcbjk9fYXUs71EEN9U0r/3bckWc8MJn4CnrJPRwydIR8TWDvS349eXnv0bu2YatRhfPslLlbOXgL1Y1/ceqGONSrOdKPXaTlDXZjuD7bVr/bbtDL2K2N/RMmewZHoTpqIjT5/IROCfj4nqiA+Lv9Rlj24f0aNbNgwkhFfEuLucxzHMy0N8+K612Q7vqTjb4QhA19PWkeLhtuPDS6tklC/4QKfxmvWDzy6cnT1hPsl2BBmnz/nLUYKFE8hTyXVsPVQEeqDwvbYCBXxfgcI9MFB43QoUlT8ysOPT60v6+wIVeIVY+dPnBU65EPaZs+6ul62I41HWlS6UnSw8Cos+cXy5WhVJLS48nV8TWn63oKWvXhRojclkOafaEFzmPQGsiuOvBpjV97yBArDO79y4mmWv0PwhRJrFjKkHY/rBlIIwpiH0VIT0WJkcMLSgCk0yXxcC/bEqn6ALTTJT8kR9GxjeBsrbu1MXh1JDeu/cD84sp3FvHGe0Is7wUubTdO44ttwR8E05iGn571ACyXmiCs2aVh6VQBrJYrWC4lkX5gtM+ZeET2Q4TJT+1x1rgWzCQE+BGMlm0JavDt4iIlIFZpu7ZuEfVhlFgiiO16JyXto0S9c4i+ngmUn2J6ZeDK/egeHV7hh1HbwvFnOwmrtFYzwt1inrgo+Q1+mQBx0nsL8v5O3YLvnukFfzzp2EVEnKv1HMQ4v4PhLTWQ98GY8/w7ChOFprqThxiuCLCFFWTz1Y4B5rK98pZ1AjZtYndN4qZvqaY7jRt2w+vEGnvIFnnULof583sH0fgP+RN8jhHifQCro95fzIm0MbvAkd3qnJn7/xY96y8wYDyU7+Tz7m43bKKi8S3sGB1uvYskJPUV2mT9TxVdt23d2Uq7aljrHr7QBH2ZXrfG4R+O8SH617LVeOLo4r18Dx2ZrXqmhrA09/x3hVb4KH8iZ4d9FV8d2j5JQCH8jocl6HrqoaxbbtgU0/cnqbU0uo9mvYbP/GqGbRkoEN62MkJtq5czOVM9XqywfbxF0tK7DOxRl1/oGDdlRM5ZAxyf1+TI2d95f5to8t7ZHaA87+G5ME6LTQM5zdUmnJ8UKjvhd0Tckwy1xd0oFTLTE96Hmrj0VgpxaBb5kF+ZnHaLqeBQG2HJjBwNPw7Ri+A3Faw7e+pXORYVx8YPwD4x8YPwrGbUOmsy2MGz+d0g8G36xQFDHqZt2aDqUwsm4gdnT8yh6d8Wyl4fO8w7m76ZNDeb22k5JrRtuz+Rb0ZZoF9UPyHtBNZLdlIlszkfi6dfznr2sGAGSguIavIt3jAIUWt98v8/XQ9itwePYf
--------------------------------------------------------------------------------
/docs/assets/solution-architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/solution-architecture.png
--------------------------------------------------------------------------------
/docs/assets/swagger-ui-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/swagger-ui-screenshot.png
--------------------------------------------------------------------------------
/examples/test_greeting_api.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ###################################################################
4 | # Script Name : test_greeting_api.sh
5 | # Description : Test the API Gateway Greeting endpoint
6 | # which was deployed as a CDK Stack.
7 | # Args :
8 | # Author : Damian McDonald
9 | ###################################################################
10 |
11 | ### check if AWS credential variables are correctly set
12 | if [ -z "${AWS_ACCESS_KEY_ID}" ]
13 | then
14 | echo "AWS credential variable AWS_ACCESS_KEY_ID is empty."
15 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
16 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
17 | fi
18 |
19 | if [ -z "${AWS_SECRET_ACCESS_KEY}" ]
20 | then
21 | echo "AWS credential variable AWS_SECRET_ACCESS_KEY is empty."
22 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
23 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
24 | fi
25 |
26 | if [ -z "${AWS_DEFAULT_REGION}" ]
27 | then
28 | echo "AWS credential variable AWS_DEFAULT_REGION is empty."
29 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
30 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
31 | fi
32 | ### check if AWS credential variables are correctly set
33 |
34 | # set the greeting you would like to send, ensure it is a single word with no spaces
35 | GREETING="world"
36 |
37 | STACK_NAME="ApiGatewayDynamicPublish"
38 |
39 | # Get the API Endpoint
40 | API_ENDPOINT_URL_EXPORT_NAME="api-gateway-dynamic-publish-url"
41 | API_ENDPOINT_URL=$(aws cloudformation --region ${AWS_DEFAULT_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${API_ENDPOINT_URL_EXPORT_NAME}'].OutputValue" --output text)
42 | API_GATEWAY_URL="${API_ENDPOINT_URL}"
43 |
44 | ################################################
45 | # TEST Greetings API
46 | ################################################
47 |
48 | echo "Testing GET ${API_GATEWAY_URL}/greeting?greeting=${GREETING}"
49 | API_RESPONSE=$(curl -sX GET ${API_GATEWAY_URL}/greeting?greeting=${GREETING})
50 |
51 | echo ""
52 | echo ${API_RESPONSE}
53 | echo ""
--------------------------------------------------------------------------------
/examples/test_ping_api.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ###################################################################
4 | # Script Name : test_ping_api.sh
5 | # Description : Test the API Gateway Ping endpoint
6 | # which was deployed as a CDK Stack.
7 | # Args :
8 | # Author : Damian McDonald
9 | ###################################################################
10 |
11 | ### check if AWS credential variables are correctly set
12 | if [ -z "${AWS_ACCESS_KEY_ID}" ]
13 | then
14 | echo "AWS credential variable AWS_ACCESS_KEY_ID is empty."
15 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
16 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
17 | fi
18 |
19 | if [ -z "${AWS_SECRET_ACCESS_KEY}" ]
20 | then
21 | echo "AWS credential variable AWS_SECRET_ACCESS_KEY is empty."
22 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
23 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
24 | fi
25 |
26 | if [ -z "${AWS_DEFAULT_REGION}" ]
27 | then
28 | echo "AWS credential variable AWS_DEFAULT_REGION is empty."
29 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
30 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
31 | fi
32 | ### check if AWS credential variables are correctly set
33 |
34 | STACK_NAME="ApiGatewayDynamicPublish"
35 |
36 | # Get the API Endpoint
37 | API_ENDPOINT_URL_EXPORT_NAME="api-gateway-dynamic-publish-url"
38 | API_ENDPOINT_URL=$(aws cloudformation --region ${AWS_DEFAULT_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${API_ENDPOINT_URL_EXPORT_NAME}'].OutputValue" --output text)
39 | API_GATEWAY_URL="${API_ENDPOINT_URL}"
40 |
41 | ################################################
42 | # TEST Ping API
43 | ################################################
44 |
45 | echo "Testing GET ${API_GATEWAY_URL}/ping"
46 | API_RESPONSE=$(curl -sX GET ${API_GATEWAY_URL}/ping)
47 |
48 | echo ""
49 | echo ${API_RESPONSE}
50 | echo ""
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api-gateway-dynamic-publish",
3 | "lockfileVersion": 2,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "aws-cdk": "^2.14.0",
9 | "express": "^4.21.0"
10 | }
11 | },
12 | "node_modules/accepts": {
13 | "version": "1.3.8",
14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
15 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
16 | "dependencies": {
17 | "mime-types": "~2.1.34",
18 | "negotiator": "0.6.3"
19 | },
20 | "engines": {
21 | "node": ">= 0.6"
22 | }
23 | },
24 | "node_modules/array-flatten": {
25 | "version": "1.1.1",
26 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
27 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
28 | },
29 | "node_modules/aws-cdk": {
30 | "version": "2.14.0",
31 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.14.0.tgz",
32 | "integrity": "sha512-JtAojkiGohbUxQjEa9k0P4Dk4IdNDLbd7S9ValpuTjoJN51+8/E34i+jUd7Fsl7bmaS/gxLafJsbJjnlBLg5cg==",
33 | "bin": {
34 | "cdk": "bin/cdk"
35 | },
36 | "engines": {
37 | "node": ">= 14.15.0"
38 | },
39 | "optionalDependencies": {
40 | "fsevents": "2.3.2"
41 | }
42 | },
43 | "node_modules/body-parser": {
44 | "version": "1.20.3",
45 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
46 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
47 | "dependencies": {
48 | "bytes": "3.1.2",
49 | "content-type": "~1.0.5",
50 | "debug": "2.6.9",
51 | "depd": "2.0.0",
52 | "destroy": "1.2.0",
53 | "http-errors": "2.0.0",
54 | "iconv-lite": "0.4.24",
55 | "on-finished": "2.4.1",
56 | "qs": "6.13.0",
57 | "raw-body": "2.5.2",
58 | "type-is": "~1.6.18",
59 | "unpipe": "1.0.0"
60 | },
61 | "engines": {
62 | "node": ">= 0.8",
63 | "npm": "1.2.8000 || >= 1.4.16"
64 | }
65 | },
66 | "node_modules/bytes": {
67 | "version": "3.1.2",
68 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
69 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
70 | "engines": {
71 | "node": ">= 0.8"
72 | }
73 | },
74 | "node_modules/call-bind": {
75 | "version": "1.0.7",
76 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
77 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
78 | "dependencies": {
79 | "es-define-property": "^1.0.0",
80 | "es-errors": "^1.3.0",
81 | "function-bind": "^1.1.2",
82 | "get-intrinsic": "^1.2.4",
83 | "set-function-length": "^1.2.1"
84 | },
85 | "engines": {
86 | "node": ">= 0.4"
87 | },
88 | "funding": {
89 | "url": "https://github.com/sponsors/ljharb"
90 | }
91 | },
92 | "node_modules/content-disposition": {
93 | "version": "0.5.4",
94 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
95 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
96 | "dependencies": {
97 | "safe-buffer": "5.2.1"
98 | },
99 | "engines": {
100 | "node": ">= 0.6"
101 | }
102 | },
103 | "node_modules/content-type": {
104 | "version": "1.0.5",
105 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
106 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
107 | "engines": {
108 | "node": ">= 0.6"
109 | }
110 | },
111 | "node_modules/cookie": {
112 | "version": "0.6.0",
113 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
114 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
115 | "engines": {
116 | "node": ">= 0.6"
117 | }
118 | },
119 | "node_modules/cookie-signature": {
120 | "version": "1.0.6",
121 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
122 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
123 | },
124 | "node_modules/debug": {
125 | "version": "2.6.9",
126 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
127 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
128 | "dependencies": {
129 | "ms": "2.0.0"
130 | }
131 | },
132 | "node_modules/define-data-property": {
133 | "version": "1.1.4",
134 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
135 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
136 | "dependencies": {
137 | "es-define-property": "^1.0.0",
138 | "es-errors": "^1.3.0",
139 | "gopd": "^1.0.1"
140 | },
141 | "engines": {
142 | "node": ">= 0.4"
143 | },
144 | "funding": {
145 | "url": "https://github.com/sponsors/ljharb"
146 | }
147 | },
148 | "node_modules/depd": {
149 | "version": "2.0.0",
150 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
151 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
152 | "engines": {
153 | "node": ">= 0.8"
154 | }
155 | },
156 | "node_modules/destroy": {
157 | "version": "1.2.0",
158 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
159 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
160 | "engines": {
161 | "node": ">= 0.8",
162 | "npm": "1.2.8000 || >= 1.4.16"
163 | }
164 | },
165 | "node_modules/ee-first": {
166 | "version": "1.1.1",
167 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
168 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
169 | },
170 | "node_modules/encodeurl": {
171 | "version": "2.0.0",
172 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
173 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
174 | "engines": {
175 | "node": ">= 0.8"
176 | }
177 | },
178 | "node_modules/es-define-property": {
179 | "version": "1.0.0",
180 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
181 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
182 | "dependencies": {
183 | "get-intrinsic": "^1.2.4"
184 | },
185 | "engines": {
186 | "node": ">= 0.4"
187 | }
188 | },
189 | "node_modules/es-errors": {
190 | "version": "1.3.0",
191 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
192 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
193 | "engines": {
194 | "node": ">= 0.4"
195 | }
196 | },
197 | "node_modules/escape-html": {
198 | "version": "1.0.3",
199 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
200 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
201 | },
202 | "node_modules/etag": {
203 | "version": "1.8.1",
204 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
205 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
206 | "engines": {
207 | "node": ">= 0.6"
208 | }
209 | },
210 | "node_modules/express": {
211 | "version": "4.21.0",
212 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
213 | "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
214 | "dependencies": {
215 | "accepts": "~1.3.8",
216 | "array-flatten": "1.1.1",
217 | "body-parser": "1.20.3",
218 | "content-disposition": "0.5.4",
219 | "content-type": "~1.0.4",
220 | "cookie": "0.6.0",
221 | "cookie-signature": "1.0.6",
222 | "debug": "2.6.9",
223 | "depd": "2.0.0",
224 | "encodeurl": "~2.0.0",
225 | "escape-html": "~1.0.3",
226 | "etag": "~1.8.1",
227 | "finalhandler": "1.3.1",
228 | "fresh": "0.5.2",
229 | "http-errors": "2.0.0",
230 | "merge-descriptors": "1.0.3",
231 | "methods": "~1.1.2",
232 | "on-finished": "2.4.1",
233 | "parseurl": "~1.3.3",
234 | "path-to-regexp": "0.1.10",
235 | "proxy-addr": "~2.0.7",
236 | "qs": "6.13.0",
237 | "range-parser": "~1.2.1",
238 | "safe-buffer": "5.2.1",
239 | "send": "0.19.0",
240 | "serve-static": "1.16.2",
241 | "setprototypeof": "1.2.0",
242 | "statuses": "2.0.1",
243 | "type-is": "~1.6.18",
244 | "utils-merge": "1.0.1",
245 | "vary": "~1.1.2"
246 | },
247 | "engines": {
248 | "node": ">= 0.10.0"
249 | }
250 | },
251 | "node_modules/finalhandler": {
252 | "version": "1.3.1",
253 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
254 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
255 | "dependencies": {
256 | "debug": "2.6.9",
257 | "encodeurl": "~2.0.0",
258 | "escape-html": "~1.0.3",
259 | "on-finished": "2.4.1",
260 | "parseurl": "~1.3.3",
261 | "statuses": "2.0.1",
262 | "unpipe": "~1.0.0"
263 | },
264 | "engines": {
265 | "node": ">= 0.8"
266 | }
267 | },
268 | "node_modules/forwarded": {
269 | "version": "0.2.0",
270 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
271 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
272 | "engines": {
273 | "node": ">= 0.6"
274 | }
275 | },
276 | "node_modules/fresh": {
277 | "version": "0.5.2",
278 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
279 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
280 | "engines": {
281 | "node": ">= 0.6"
282 | }
283 | },
284 | "node_modules/fsevents": {
285 | "version": "2.3.2",
286 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
287 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
288 | "hasInstallScript": true,
289 | "optional": true,
290 | "os": [
291 | "darwin"
292 | ],
293 | "engines": {
294 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
295 | }
296 | },
297 | "node_modules/function-bind": {
298 | "version": "1.1.2",
299 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
300 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
301 | "funding": {
302 | "url": "https://github.com/sponsors/ljharb"
303 | }
304 | },
305 | "node_modules/get-intrinsic": {
306 | "version": "1.2.4",
307 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
308 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
309 | "dependencies": {
310 | "es-errors": "^1.3.0",
311 | "function-bind": "^1.1.2",
312 | "has-proto": "^1.0.1",
313 | "has-symbols": "^1.0.3",
314 | "hasown": "^2.0.0"
315 | },
316 | "engines": {
317 | "node": ">= 0.4"
318 | },
319 | "funding": {
320 | "url": "https://github.com/sponsors/ljharb"
321 | }
322 | },
323 | "node_modules/gopd": {
324 | "version": "1.0.1",
325 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
326 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
327 | "dependencies": {
328 | "get-intrinsic": "^1.1.3"
329 | },
330 | "funding": {
331 | "url": "https://github.com/sponsors/ljharb"
332 | }
333 | },
334 | "node_modules/has-property-descriptors": {
335 | "version": "1.0.2",
336 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
337 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
338 | "dependencies": {
339 | "es-define-property": "^1.0.0"
340 | },
341 | "funding": {
342 | "url": "https://github.com/sponsors/ljharb"
343 | }
344 | },
345 | "node_modules/has-proto": {
346 | "version": "1.0.3",
347 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
348 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
349 | "engines": {
350 | "node": ">= 0.4"
351 | },
352 | "funding": {
353 | "url": "https://github.com/sponsors/ljharb"
354 | }
355 | },
356 | "node_modules/has-symbols": {
357 | "version": "1.0.3",
358 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
359 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
360 | "engines": {
361 | "node": ">= 0.4"
362 | },
363 | "funding": {
364 | "url": "https://github.com/sponsors/ljharb"
365 | }
366 | },
367 | "node_modules/hasown": {
368 | "version": "2.0.2",
369 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
370 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
371 | "dependencies": {
372 | "function-bind": "^1.1.2"
373 | },
374 | "engines": {
375 | "node": ">= 0.4"
376 | }
377 | },
378 | "node_modules/http-errors": {
379 | "version": "2.0.0",
380 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
381 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
382 | "dependencies": {
383 | "depd": "2.0.0",
384 | "inherits": "2.0.4",
385 | "setprototypeof": "1.2.0",
386 | "statuses": "2.0.1",
387 | "toidentifier": "1.0.1"
388 | },
389 | "engines": {
390 | "node": ">= 0.8"
391 | }
392 | },
393 | "node_modules/iconv-lite": {
394 | "version": "0.4.24",
395 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
396 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
397 | "dependencies": {
398 | "safer-buffer": ">= 2.1.2 < 3"
399 | },
400 | "engines": {
401 | "node": ">=0.10.0"
402 | }
403 | },
404 | "node_modules/inherits": {
405 | "version": "2.0.4",
406 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
407 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
408 | },
409 | "node_modules/ipaddr.js": {
410 | "version": "1.9.1",
411 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
412 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
413 | "engines": {
414 | "node": ">= 0.10"
415 | }
416 | },
417 | "node_modules/media-typer": {
418 | "version": "0.3.0",
419 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
420 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
421 | "engines": {
422 | "node": ">= 0.6"
423 | }
424 | },
425 | "node_modules/merge-descriptors": {
426 | "version": "1.0.3",
427 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
428 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
429 | "funding": {
430 | "url": "https://github.com/sponsors/sindresorhus"
431 | }
432 | },
433 | "node_modules/methods": {
434 | "version": "1.1.2",
435 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
436 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
437 | "engines": {
438 | "node": ">= 0.6"
439 | }
440 | },
441 | "node_modules/mime": {
442 | "version": "1.6.0",
443 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
444 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
445 | "bin": {
446 | "mime": "cli.js"
447 | },
448 | "engines": {
449 | "node": ">=4"
450 | }
451 | },
452 | "node_modules/mime-db": {
453 | "version": "1.51.0",
454 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
455 | "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
456 | "engines": {
457 | "node": ">= 0.6"
458 | }
459 | },
460 | "node_modules/mime-types": {
461 | "version": "2.1.34",
462 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
463 | "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
464 | "dependencies": {
465 | "mime-db": "1.51.0"
466 | },
467 | "engines": {
468 | "node": ">= 0.6"
469 | }
470 | },
471 | "node_modules/ms": {
472 | "version": "2.0.0",
473 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
474 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
475 | },
476 | "node_modules/negotiator": {
477 | "version": "0.6.3",
478 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
479 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
480 | "engines": {
481 | "node": ">= 0.6"
482 | }
483 | },
484 | "node_modules/object-inspect": {
485 | "version": "1.13.2",
486 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
487 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
488 | "engines": {
489 | "node": ">= 0.4"
490 | },
491 | "funding": {
492 | "url": "https://github.com/sponsors/ljharb"
493 | }
494 | },
495 | "node_modules/on-finished": {
496 | "version": "2.4.1",
497 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
498 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
499 | "dependencies": {
500 | "ee-first": "1.1.1"
501 | },
502 | "engines": {
503 | "node": ">= 0.8"
504 | }
505 | },
506 | "node_modules/parseurl": {
507 | "version": "1.3.3",
508 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
509 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
510 | "engines": {
511 | "node": ">= 0.8"
512 | }
513 | },
514 | "node_modules/path-to-regexp": {
515 | "version": "0.1.10",
516 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
517 | "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
518 | },
519 | "node_modules/proxy-addr": {
520 | "version": "2.0.7",
521 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
522 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
523 | "dependencies": {
524 | "forwarded": "0.2.0",
525 | "ipaddr.js": "1.9.1"
526 | },
527 | "engines": {
528 | "node": ">= 0.10"
529 | }
530 | },
531 | "node_modules/qs": {
532 | "version": "6.13.0",
533 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
534 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
535 | "dependencies": {
536 | "side-channel": "^1.0.6"
537 | },
538 | "engines": {
539 | "node": ">=0.6"
540 | },
541 | "funding": {
542 | "url": "https://github.com/sponsors/ljharb"
543 | }
544 | },
545 | "node_modules/range-parser": {
546 | "version": "1.2.1",
547 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
548 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
549 | "engines": {
550 | "node": ">= 0.6"
551 | }
552 | },
553 | "node_modules/raw-body": {
554 | "version": "2.5.2",
555 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
556 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
557 | "dependencies": {
558 | "bytes": "3.1.2",
559 | "http-errors": "2.0.0",
560 | "iconv-lite": "0.4.24",
561 | "unpipe": "1.0.0"
562 | },
563 | "engines": {
564 | "node": ">= 0.8"
565 | }
566 | },
567 | "node_modules/safe-buffer": {
568 | "version": "5.2.1",
569 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
570 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
571 | "funding": [
572 | {
573 | "type": "github",
574 | "url": "https://github.com/sponsors/feross"
575 | },
576 | {
577 | "type": "patreon",
578 | "url": "https://www.patreon.com/feross"
579 | },
580 | {
581 | "type": "consulting",
582 | "url": "https://feross.org/support"
583 | }
584 | ]
585 | },
586 | "node_modules/safer-buffer": {
587 | "version": "2.1.2",
588 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
589 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
590 | },
591 | "node_modules/send": {
592 | "version": "0.19.0",
593 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
594 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
595 | "dependencies": {
596 | "debug": "2.6.9",
597 | "depd": "2.0.0",
598 | "destroy": "1.2.0",
599 | "encodeurl": "~1.0.2",
600 | "escape-html": "~1.0.3",
601 | "etag": "~1.8.1",
602 | "fresh": "0.5.2",
603 | "http-errors": "2.0.0",
604 | "mime": "1.6.0",
605 | "ms": "2.1.3",
606 | "on-finished": "2.4.1",
607 | "range-parser": "~1.2.1",
608 | "statuses": "2.0.1"
609 | },
610 | "engines": {
611 | "node": ">= 0.8.0"
612 | }
613 | },
614 | "node_modules/send/node_modules/encodeurl": {
615 | "version": "1.0.2",
616 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
617 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
618 | "engines": {
619 | "node": ">= 0.8"
620 | }
621 | },
622 | "node_modules/send/node_modules/ms": {
623 | "version": "2.1.3",
624 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
625 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
626 | },
627 | "node_modules/serve-static": {
628 | "version": "1.16.2",
629 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
630 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
631 | "dependencies": {
632 | "encodeurl": "~2.0.0",
633 | "escape-html": "~1.0.3",
634 | "parseurl": "~1.3.3",
635 | "send": "0.19.0"
636 | },
637 | "engines": {
638 | "node": ">= 0.8.0"
639 | }
640 | },
641 | "node_modules/set-function-length": {
642 | "version": "1.2.2",
643 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
644 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
645 | "dependencies": {
646 | "define-data-property": "^1.1.4",
647 | "es-errors": "^1.3.0",
648 | "function-bind": "^1.1.2",
649 | "get-intrinsic": "^1.2.4",
650 | "gopd": "^1.0.1",
651 | "has-property-descriptors": "^1.0.2"
652 | },
653 | "engines": {
654 | "node": ">= 0.4"
655 | }
656 | },
657 | "node_modules/setprototypeof": {
658 | "version": "1.2.0",
659 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
660 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
661 | },
662 | "node_modules/side-channel": {
663 | "version": "1.0.6",
664 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
665 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
666 | "dependencies": {
667 | "call-bind": "^1.0.7",
668 | "es-errors": "^1.3.0",
669 | "get-intrinsic": "^1.2.4",
670 | "object-inspect": "^1.13.1"
671 | },
672 | "engines": {
673 | "node": ">= 0.4"
674 | },
675 | "funding": {
676 | "url": "https://github.com/sponsors/ljharb"
677 | }
678 | },
679 | "node_modules/statuses": {
680 | "version": "2.0.1",
681 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
682 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
683 | "engines": {
684 | "node": ">= 0.8"
685 | }
686 | },
687 | "node_modules/toidentifier": {
688 | "version": "1.0.1",
689 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
690 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
691 | "engines": {
692 | "node": ">=0.6"
693 | }
694 | },
695 | "node_modules/type-is": {
696 | "version": "1.6.18",
697 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
698 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
699 | "dependencies": {
700 | "media-typer": "0.3.0",
701 | "mime-types": "~2.1.24"
702 | },
703 | "engines": {
704 | "node": ">= 0.6"
705 | }
706 | },
707 | "node_modules/unpipe": {
708 | "version": "1.0.0",
709 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
710 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
711 | "engines": {
712 | "node": ">= 0.8"
713 | }
714 | },
715 | "node_modules/utils-merge": {
716 | "version": "1.0.1",
717 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
718 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
719 | "engines": {
720 | "node": ">= 0.4.0"
721 | }
722 | },
723 | "node_modules/vary": {
724 | "version": "1.1.2",
725 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
726 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
727 | "engines": {
728 | "node": ">= 0.8"
729 | }
730 | }
731 | },
732 | "dependencies": {
733 | "accepts": {
734 | "version": "1.3.8",
735 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
736 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
737 | "requires": {
738 | "mime-types": "~2.1.34",
739 | "negotiator": "0.6.3"
740 | }
741 | },
742 | "array-flatten": {
743 | "version": "1.1.1",
744 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
745 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
746 | },
747 | "aws-cdk": {
748 | "version": "2.14.0",
749 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.14.0.tgz",
750 | "integrity": "sha512-JtAojkiGohbUxQjEa9k0P4Dk4IdNDLbd7S9ValpuTjoJN51+8/E34i+jUd7Fsl7bmaS/gxLafJsbJjnlBLg5cg==",
751 | "requires": {
752 | "fsevents": "2.3.2"
753 | }
754 | },
755 | "body-parser": {
756 | "version": "1.20.3",
757 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
758 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
759 | "requires": {
760 | "bytes": "3.1.2",
761 | "content-type": "~1.0.5",
762 | "debug": "2.6.9",
763 | "depd": "2.0.0",
764 | "destroy": "1.2.0",
765 | "http-errors": "2.0.0",
766 | "iconv-lite": "0.4.24",
767 | "on-finished": "2.4.1",
768 | "qs": "6.13.0",
769 | "raw-body": "2.5.2",
770 | "type-is": "~1.6.18",
771 | "unpipe": "1.0.0"
772 | }
773 | },
774 | "bytes": {
775 | "version": "3.1.2",
776 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
777 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
778 | },
779 | "call-bind": {
780 | "version": "1.0.7",
781 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
782 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
783 | "requires": {
784 | "es-define-property": "^1.0.0",
785 | "es-errors": "^1.3.0",
786 | "function-bind": "^1.1.2",
787 | "get-intrinsic": "^1.2.4",
788 | "set-function-length": "^1.2.1"
789 | }
790 | },
791 | "content-disposition": {
792 | "version": "0.5.4",
793 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
794 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
795 | "requires": {
796 | "safe-buffer": "5.2.1"
797 | }
798 | },
799 | "content-type": {
800 | "version": "1.0.5",
801 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
802 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
803 | },
804 | "cookie": {
805 | "version": "0.6.0",
806 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
807 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
808 | },
809 | "cookie-signature": {
810 | "version": "1.0.6",
811 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
812 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
813 | },
814 | "debug": {
815 | "version": "2.6.9",
816 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
817 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
818 | "requires": {
819 | "ms": "2.0.0"
820 | }
821 | },
822 | "define-data-property": {
823 | "version": "1.1.4",
824 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
825 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
826 | "requires": {
827 | "es-define-property": "^1.0.0",
828 | "es-errors": "^1.3.0",
829 | "gopd": "^1.0.1"
830 | }
831 | },
832 | "depd": {
833 | "version": "2.0.0",
834 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
835 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
836 | },
837 | "destroy": {
838 | "version": "1.2.0",
839 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
840 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
841 | },
842 | "ee-first": {
843 | "version": "1.1.1",
844 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
845 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
846 | },
847 | "encodeurl": {
848 | "version": "2.0.0",
849 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
850 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
851 | },
852 | "es-define-property": {
853 | "version": "1.0.0",
854 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
855 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
856 | "requires": {
857 | "get-intrinsic": "^1.2.4"
858 | }
859 | },
860 | "es-errors": {
861 | "version": "1.3.0",
862 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
863 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
864 | },
865 | "escape-html": {
866 | "version": "1.0.3",
867 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
868 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
869 | },
870 | "etag": {
871 | "version": "1.8.1",
872 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
873 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
874 | },
875 | "express": {
876 | "version": "4.21.0",
877 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
878 | "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
879 | "requires": {
880 | "accepts": "~1.3.8",
881 | "array-flatten": "1.1.1",
882 | "body-parser": "1.20.3",
883 | "content-disposition": "0.5.4",
884 | "content-type": "~1.0.4",
885 | "cookie": "0.6.0",
886 | "cookie-signature": "1.0.6",
887 | "debug": "2.6.9",
888 | "depd": "2.0.0",
889 | "encodeurl": "~2.0.0",
890 | "escape-html": "~1.0.3",
891 | "etag": "~1.8.1",
892 | "finalhandler": "1.3.1",
893 | "fresh": "0.5.2",
894 | "http-errors": "2.0.0",
895 | "merge-descriptors": "1.0.3",
896 | "methods": "~1.1.2",
897 | "on-finished": "2.4.1",
898 | "parseurl": "~1.3.3",
899 | "path-to-regexp": "0.1.10",
900 | "proxy-addr": "~2.0.7",
901 | "qs": "6.13.0",
902 | "range-parser": "~1.2.1",
903 | "safe-buffer": "5.2.1",
904 | "send": "0.19.0",
905 | "serve-static": "1.16.2",
906 | "setprototypeof": "1.2.0",
907 | "statuses": "2.0.1",
908 | "type-is": "~1.6.18",
909 | "utils-merge": "1.0.1",
910 | "vary": "~1.1.2"
911 | }
912 | },
913 | "finalhandler": {
914 | "version": "1.3.1",
915 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
916 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
917 | "requires": {
918 | "debug": "2.6.9",
919 | "encodeurl": "~2.0.0",
920 | "escape-html": "~1.0.3",
921 | "on-finished": "2.4.1",
922 | "parseurl": "~1.3.3",
923 | "statuses": "2.0.1",
924 | "unpipe": "~1.0.0"
925 | }
926 | },
927 | "forwarded": {
928 | "version": "0.2.0",
929 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
930 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
931 | },
932 | "fresh": {
933 | "version": "0.5.2",
934 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
935 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
936 | },
937 | "fsevents": {
938 | "version": "2.3.2",
939 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
940 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
941 | "optional": true
942 | },
943 | "function-bind": {
944 | "version": "1.1.2",
945 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
946 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
947 | },
948 | "get-intrinsic": {
949 | "version": "1.2.4",
950 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
951 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
952 | "requires": {
953 | "es-errors": "^1.3.0",
954 | "function-bind": "^1.1.2",
955 | "has-proto": "^1.0.1",
956 | "has-symbols": "^1.0.3",
957 | "hasown": "^2.0.0"
958 | }
959 | },
960 | "gopd": {
961 | "version": "1.0.1",
962 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
963 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
964 | "requires": {
965 | "get-intrinsic": "^1.1.3"
966 | }
967 | },
968 | "has-property-descriptors": {
969 | "version": "1.0.2",
970 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
971 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
972 | "requires": {
973 | "es-define-property": "^1.0.0"
974 | }
975 | },
976 | "has-proto": {
977 | "version": "1.0.3",
978 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
979 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="
980 | },
981 | "has-symbols": {
982 | "version": "1.0.3",
983 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
984 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
985 | },
986 | "hasown": {
987 | "version": "2.0.2",
988 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
989 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
990 | "requires": {
991 | "function-bind": "^1.1.2"
992 | }
993 | },
994 | "http-errors": {
995 | "version": "2.0.0",
996 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
997 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
998 | "requires": {
999 | "depd": "2.0.0",
1000 | "inherits": "2.0.4",
1001 | "setprototypeof": "1.2.0",
1002 | "statuses": "2.0.1",
1003 | "toidentifier": "1.0.1"
1004 | }
1005 | },
1006 | "iconv-lite": {
1007 | "version": "0.4.24",
1008 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1009 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1010 | "requires": {
1011 | "safer-buffer": ">= 2.1.2 < 3"
1012 | }
1013 | },
1014 | "inherits": {
1015 | "version": "2.0.4",
1016 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1017 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
1018 | },
1019 | "ipaddr.js": {
1020 | "version": "1.9.1",
1021 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1022 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
1023 | },
1024 | "media-typer": {
1025 | "version": "0.3.0",
1026 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1027 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
1028 | },
1029 | "merge-descriptors": {
1030 | "version": "1.0.3",
1031 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1032 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
1033 | },
1034 | "methods": {
1035 | "version": "1.1.2",
1036 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1037 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
1038 | },
1039 | "mime": {
1040 | "version": "1.6.0",
1041 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1042 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
1043 | },
1044 | "mime-db": {
1045 | "version": "1.51.0",
1046 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
1047 | "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
1048 | },
1049 | "mime-types": {
1050 | "version": "2.1.34",
1051 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
1052 | "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
1053 | "requires": {
1054 | "mime-db": "1.51.0"
1055 | }
1056 | },
1057 | "ms": {
1058 | "version": "2.0.0",
1059 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1060 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
1061 | },
1062 | "negotiator": {
1063 | "version": "0.6.3",
1064 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1065 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
1066 | },
1067 | "object-inspect": {
1068 | "version": "1.13.2",
1069 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
1070 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
1071 | },
1072 | "on-finished": {
1073 | "version": "2.4.1",
1074 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1075 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1076 | "requires": {
1077 | "ee-first": "1.1.1"
1078 | }
1079 | },
1080 | "parseurl": {
1081 | "version": "1.3.3",
1082 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1083 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
1084 | },
1085 | "path-to-regexp": {
1086 | "version": "0.1.10",
1087 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
1088 | "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
1089 | },
1090 | "proxy-addr": {
1091 | "version": "2.0.7",
1092 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1093 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1094 | "requires": {
1095 | "forwarded": "0.2.0",
1096 | "ipaddr.js": "1.9.1"
1097 | }
1098 | },
1099 | "qs": {
1100 | "version": "6.13.0",
1101 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
1102 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
1103 | "requires": {
1104 | "side-channel": "^1.0.6"
1105 | }
1106 | },
1107 | "range-parser": {
1108 | "version": "1.2.1",
1109 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1110 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
1111 | },
1112 | "raw-body": {
1113 | "version": "2.5.2",
1114 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1115 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1116 | "requires": {
1117 | "bytes": "3.1.2",
1118 | "http-errors": "2.0.0",
1119 | "iconv-lite": "0.4.24",
1120 | "unpipe": "1.0.0"
1121 | }
1122 | },
1123 | "safe-buffer": {
1124 | "version": "5.2.1",
1125 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1126 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
1127 | },
1128 | "safer-buffer": {
1129 | "version": "2.1.2",
1130 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1131 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1132 | },
1133 | "send": {
1134 | "version": "0.19.0",
1135 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1136 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1137 | "requires": {
1138 | "debug": "2.6.9",
1139 | "depd": "2.0.0",
1140 | "destroy": "1.2.0",
1141 | "encodeurl": "~1.0.2",
1142 | "escape-html": "~1.0.3",
1143 | "etag": "~1.8.1",
1144 | "fresh": "0.5.2",
1145 | "http-errors": "2.0.0",
1146 | "mime": "1.6.0",
1147 | "ms": "2.1.3",
1148 | "on-finished": "2.4.1",
1149 | "range-parser": "~1.2.1",
1150 | "statuses": "2.0.1"
1151 | },
1152 | "dependencies": {
1153 | "encodeurl": {
1154 | "version": "1.0.2",
1155 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1156 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
1157 | },
1158 | "ms": {
1159 | "version": "2.1.3",
1160 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1161 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1162 | }
1163 | }
1164 | },
1165 | "serve-static": {
1166 | "version": "1.16.2",
1167 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1168 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1169 | "requires": {
1170 | "encodeurl": "~2.0.0",
1171 | "escape-html": "~1.0.3",
1172 | "parseurl": "~1.3.3",
1173 | "send": "0.19.0"
1174 | }
1175 | },
1176 | "set-function-length": {
1177 | "version": "1.2.2",
1178 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
1179 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
1180 | "requires": {
1181 | "define-data-property": "^1.1.4",
1182 | "es-errors": "^1.3.0",
1183 | "function-bind": "^1.1.2",
1184 | "get-intrinsic": "^1.2.4",
1185 | "gopd": "^1.0.1",
1186 | "has-property-descriptors": "^1.0.2"
1187 | }
1188 | },
1189 | "setprototypeof": {
1190 | "version": "1.2.0",
1191 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1192 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1193 | },
1194 | "side-channel": {
1195 | "version": "1.0.6",
1196 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
1197 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
1198 | "requires": {
1199 | "call-bind": "^1.0.7",
1200 | "es-errors": "^1.3.0",
1201 | "get-intrinsic": "^1.2.4",
1202 | "object-inspect": "^1.13.1"
1203 | }
1204 | },
1205 | "statuses": {
1206 | "version": "2.0.1",
1207 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1208 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
1209 | },
1210 | "toidentifier": {
1211 | "version": "1.0.1",
1212 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1213 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
1214 | },
1215 | "type-is": {
1216 | "version": "1.6.18",
1217 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1218 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1219 | "requires": {
1220 | "media-typer": "0.3.0",
1221 | "mime-types": "~2.1.24"
1222 | }
1223 | },
1224 | "unpipe": {
1225 | "version": "1.0.0",
1226 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1227 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
1228 | },
1229 | "utils-merge": {
1230 | "version": "1.0.1",
1231 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1232 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
1233 | },
1234 | "vary": {
1235 | "version": "1.1.2",
1236 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1237 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
1238 | }
1239 | }
1240 | }
1241 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "aws-cdk": "^2.14.0",
4 | "express": "^4.21.0"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiodns==3.0.0
2 | aiohttp==3.10.2
3 | aiomultiprocess==0.9.0
4 | aiosignal==1.2.0
5 | argcomplete==2.0.0
6 | async-timeout==4.0.2
7 | attrs==21.4.0
8 | aws-cdk-lib==2.14.0
9 | bc-python-hcl2==0.3.39
10 | beautifulsoup4==4.11.1
11 | boto3==1.21.8
12 | botocore==1.24.8
13 | cached-property==1.5.2
14 | cachetools==5.0.0
15 | cattrs==1.10.0
16 | certifi==2024.7.4
17 | cffi==1.15.0
18 | charset-normalizer==2.0.12
19 | checkov==2.0.1098
20 | click==8.1.3
21 | click-option-group==0.5.3
22 | cloudsplaining==0.5.0
23 | colorama==0.4.4
24 | ConfigArgParse==1.5.3
25 | constructs==10.0.73
26 | contextlib2==21.6.0
27 | cyclonedx-python-lib==0.12.3
28 | decorator==5.1.1
29 | deep-merge==0.0.4
30 | detect-secrets==1.2.0
31 | docker==5.0.3
32 | dockerfile-parse==1.2.0
33 | dpath==1.5.0
34 | frozenlist==1.3.0
35 | gitdb==4.0.9
36 | GitPython==3.1.41
37 | idna==3.7
38 | importlib-metadata==4.11.3
39 | iniconfig==1.1.1
40 | Jinja2==3.1.4
41 | jmespath==0.10.0
42 | jsii==1.54.0
43 | jsonpath-ng==1.5.3
44 | jsonschema==3.2.0
45 | junit-xml==1.9
46 | lark==1.1.2
47 | Markdown==3.3.6
48 | MarkupSafe==2.1.1
49 | multidict==6.0.2
50 | networkx==2.8
51 | packageurl-python==0.9.9
52 | packaging==21.3
53 | pluggy==1.0.0
54 | ply==3.11
55 | policy-sentry==0.12.3
56 | policyuniverse==1.5.0.20220426
57 | prettytable==3.2.0
58 | publication==0.0.3
59 | py==1.11.0
60 | pycares==4.2.0
61 | pycep-parser==0.3.4
62 | pycparser==2.21
63 | pyparsing==3.0.7
64 | pyrsistent==0.18.1
65 | pytest==7.0.1
66 | python-dateutil==2.8.2
67 | PyYAML==6.0
68 | regex==2022.4.24
69 | requests==2.32.2
70 | s3transfer==0.5.2
71 | schema==0.7.5
72 | semantic-version==2.9.0
73 | six==1.16.0
74 | smmap==5.0.0
75 | soupsieve==2.3.2.post1
76 | tabulate==0.8.9
77 | termcolor==1.1.0
78 | toml==0.10.2
79 | tomli==2.0.1
80 | tqdm==4.66.3
81 | types-setuptools==57.4.14
82 | types-toml==0.10.6
83 | typing_extensions==4.1.1
84 | update-checker==0.18.0
85 | urllib3==1.26.19
86 | wcwidth==0.2.5
87 | websocket-client==1.3.2
88 | yarl==1.7.2
89 | zipp==3.19.1
90 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 |
4 | with open("README.md") as fp:
5 | long_description = fp.read()
6 |
7 |
8 | setuptools.setup(
9 | name="api-gateway-dynamic-publish",
10 | version="0.0.1",
11 |
12 | description="A sample CDK Python app",
13 | long_description=long_description,
14 | long_description_content_type="text/markdown",
15 |
16 | author="author",
17 |
18 | package_dir={"": "api-gateway-dynamic-publish"},
19 | packages=setuptools.find_packages(where="api-gateway-dynamic-publish"),
20 |
21 | install_requires=[
22 | "aws-cdk.core==2.14.0",
23 | "aws-cdk.aws_iam==2.14.0",
24 | "aws-cdk.aws_lambda==2.14.0",
25 | "aws-cdk.aws_logs==2.14.0",
26 | "aws-cdk.custom_resources==2.14.0",
27 | "aws-cdk.aws_s3==2.14.0",
28 | ],
29 |
30 | python_requires=">=3.6",
31 |
32 | classifiers=[
33 | "Development Status :: 4 - Beta",
34 |
35 | "Intended Audience :: Developers",
36 |
37 | "Programming Language :: JavaScript",
38 | "Programming Language :: Python :: 3 :: Only",
39 | "Programming Language :: Python :: 3.6",
40 | "Programming Language :: Python :: 3.7",
41 | "Programming Language :: Python :: 3.8",
42 |
43 | "Topic :: Software Development :: Code Generators",
44 | "Topic :: Utilities",
45 |
46 | "Typing :: Typed",
47 | ],
48 | )
49 |
--------------------------------------------------------------------------------
/source.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem The sole purpose of this script is to make the command
4 | rem
5 | rem source .venv/bin/activate
6 | rem
7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows.
8 | rem On Windows, this command just runs this batch file (the argument is ignored).
9 | rem
10 | rem Now we don't need to document a Windows command for activating a virtualenv.
11 |
12 | echo Executing .venv\Scripts\activate.bat for you
13 | .venv\Scripts\activate.bat
14 |
--------------------------------------------------------------------------------
/stacks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/stacks/__init__.py
--------------------------------------------------------------------------------
/stacks/apigateway_dynamic_publish.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | apigateway_dynamic_publish.py:
5 | CDK Stack that creates and deploys the infrastructure
6 | required for the api-gateway-dynamic-publish project.
7 | """
8 |
9 | import json
10 | import os
11 |
12 | from aws_cdk import (
13 | BundlingOptions,
14 | CfnOutput,
15 | CustomResource,
16 | Duration,
17 | RemovalPolicy,
18 | Stack
19 | )
20 | from aws_cdk import aws_iam as iam
21 | from aws_cdk import aws_lambda
22 | from aws_cdk import aws_logs as logs
23 | from aws_cdk import aws_s3 as s3
24 | from aws_cdk import custom_resources
25 | from constructs import Construct
26 |
27 |
28 | class ApiGatewayDynamicPublishStack(Stack):
29 | """
30 | CDK Stack that creates and deploys the infrastructure
31 | required for the api-gateway-dynamic-publish project.
32 | """
33 |
34 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
35 | super().__init__(scope, construct_id, **kwargs)
36 |
37 | config = self.read_cdk_context_json()
38 |
39 | ##########################################################
40 | # API Gateway Documentation Bucket
41 | ##########################################################
42 |
43 | api_documentation_bucket = s3.Bucket(
44 | self,
45 | 'ApiDocumentationBucket',
46 | encryption=s3.BucketEncryption.KMS,
47 | removal_policy=RemovalPolicy.DESTROY,
48 | auto_delete_objects=True,
49 | block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
50 | public_read_access=False,
51 | versioned=True
52 | )
53 |
54 | ##########################################################
55 | # API Gateway Documentation Bucket
56 | ##########################################################
57 |
58 |
59 | ##########################################################
60 | # API Gateway Access Logs Group
61 | ##########################################################
62 |
63 | # create log group for API Gateway access logs
64 | api_gateway_access_log_group = logs.LogGroup(
65 | self,
66 | 'ApiGatewayAccessLogGroup',
67 | log_group_name='/aws/vendedlogs/ApiGatewayAccessLogs',
68 | removal_policy=RemovalPolicy.DESTROY,
69 | retention=logs.RetentionDays.TWO_WEEKS
70 | )
71 |
72 |
73 | ##########################################################
74 | # API Gateway Lambda Integrations
75 | ##########################################################
76 |
77 | # IAM Role for Lambda
78 | api_gateway_integration_lambda_role = iam.Role(
79 | scope=self,
80 | id="ApiGatewayIntegrationLambdaRole",
81 | assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
82 | managed_policies=[
83 | iam.ManagedPolicy.from_aws_managed_policy_name(
84 | "service-role/AWSLambdaBasicExecutionRole"
85 | )
86 | ]
87 | )
88 |
89 | api_gateway_integration_lambda_role.add_to_policy(
90 | iam.PolicyStatement(
91 | effect=iam.Effect.ALLOW,
92 | resources=["*"],
93 | actions=[
94 | "logs:CreateLogGroup",
95 | "logs:CreateLogStream",
96 | "logs:PutLogEvents",
97 | "logs:ListLogDeliveries"
98 | ]
99 | )
100 | )
101 |
102 | api_gateway_ping_lambda = aws_lambda.Function(
103 | scope=self,
104 | id="ApiGatewayPingLambda",
105 | code=aws_lambda.Code.from_asset(f"{os.path.dirname(__file__)}/resources/api_integrations"),
106 | handler="ping.lambda_handler",
107 | role=api_gateway_integration_lambda_role,
108 | runtime=aws_lambda.Runtime.PYTHON_3_9,
109 | timeout=Duration.seconds(15)
110 | )
111 |
112 | api_gateway_greeting_lambda = aws_lambda.Function(
113 | scope=self,
114 | id="ApiGatewayGreetingLambda",
115 | code=aws_lambda.Code.from_asset(f"{os.path.dirname(__file__)}/resources/api_integrations"),
116 | handler="greeting.lambda_handler",
117 | role=api_gateway_integration_lambda_role,
118 | runtime=aws_lambda.Runtime.PYTHON_3_9,
119 | timeout=Duration.seconds(15)
120 | )
121 |
122 | ##########################################################
123 | # Create API Creator Custom Resource
124 | ##########################################################
125 |
126 |
127 | ##########################################################
128 | # Create API Creator Custom Resource
129 | ##########################################################
130 |
131 | # Create a role for the api creator lambda function
132 | apicreator_lambda_role = iam.Role(
133 | scope=self,
134 | id="ApiCreatorLambdaRole",
135 | assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
136 | managed_policies=[
137 | iam.ManagedPolicy.from_aws_managed_policy_name(
138 | "service-role/AWSLambdaBasicExecutionRole"
139 | )
140 | ]
141 | )
142 |
143 | apicreator_lambda_role.add_to_policy(
144 | iam.PolicyStatement(
145 | effect=iam.Effect.ALLOW,
146 | resources=[
147 | "arn:aws:apigateway:*::/apis/*",
148 | "arn:aws:apigateway:*::/apis"
149 | ],
150 | actions=[
151 | "apigateway:DELETE",
152 | "apigateway:PUT",
153 | "apigateway:PATCH",
154 | "apigateway:POST",
155 | "apigateway:GET"
156 | ]
157 | )
158 | )
159 |
160 | apicreator_lambda_role.add_to_policy(
161 | iam.PolicyStatement(
162 | effect=iam.Effect.ALLOW,
163 | resources=["*"],
164 | actions=[
165 | "logs:*"
166 | ]
167 | )
168 | )
169 |
170 | api_documentation_bucket.grant_read_write(apicreator_lambda_role)
171 |
172 | apicreator_lambda = aws_lambda.Function(
173 | scope=self,
174 | id="ApiCreatorLambda",
175 | code=aws_lambda.Code.from_asset(
176 | f"{os.path.dirname(__file__)}/resources/api_creation",
177 | bundling=BundlingOptions(
178 | image=aws_lambda.Runtime.PYTHON_3_9.bundling_image,
179 | command=[
180 | "bash", "-c",
181 | "pip install --no-cache -r requirements.txt -t /asset-output && cp -au . /asset-output"
182 | ],
183 | ),
184 | ),
185 | handler="api_creator.lambda_handler",
186 | role=apicreator_lambda_role,
187 | runtime=aws_lambda.Runtime.PYTHON_3_9,
188 | timeout=Duration.minutes(5)
189 | )
190 |
191 | # Provider that invokes the api creator lambda function
192 | apicreator_provider = custom_resources.Provider(
193 | self,
194 | 'ApiCreatorCustomResourceProvider',
195 | on_event_handler=apicreator_lambda
196 | )
197 |
198 | # The custom resource that uses the api creator provider to supply values
199 | apicreator_custom_resource = CustomResource(
200 | self,
201 | 'ApiCreatorCustomResource',
202 | service_token=apicreator_provider.service_token,
203 | properties={
204 | 'ApiGatewayAccessLogsLogGroupArn': api_gateway_access_log_group.log_group_arn,
205 | 'ApiIntegrationPingLambda': api_gateway_ping_lambda.function_arn,
206 | 'ApiIntegrationGreetingLambda': api_gateway_greeting_lambda.function_arn,
207 | 'ApiDocumentationBucketName': api_documentation_bucket.bucket_name,
208 | 'ApiDocumentationBucketUrl': api_documentation_bucket.bucket_website_url,
209 | 'ApiName': f"{config['api']['apiName']}",
210 | 'ApiStageName': config['api']['apiStageName'],
211 | 'ThrottlingBurstLimit': config['api']['throttlingBurstLimit'],
212 | 'ThrottlingRateLimit': config['api']['throttlingRateLimit']
213 | }
214 | )
215 |
216 | ##########################################################
217 | # Create API Creator Custom Resource
218 | ##########################################################
219 |
220 | ##########################################################
221 | # Create AWS API Gateway permissions
222 | ##########################################################
223 |
224 | apigateway_id = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiId')
225 | apigateway_endpoint = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiEndpoint')
226 | apigateway_stagename = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiStageName')
227 |
228 | http_api_arn = (
229 | f"arn:{self.partition}:execute-api:"
230 | f"{self.region}:{self.account}:"
231 | f"{apigateway_id}/*/*/*"
232 | )
233 |
234 | # grant HttpApi permission to invoke api lambda function
235 | api_gateway_ping_lambda.add_permission(
236 | f"Invoke By Orchestrator Gateway Permission",
237 | principal=iam.ServicePrincipal("apigateway.amazonaws.com"),
238 | action="lambda:InvokeFunction",
239 | source_arn=http_api_arn
240 | )
241 |
242 | api_gateway_greeting_lambda.add_permission(
243 | f"Invoke By Orchestrator Gateway Permission",
244 | principal=iam.ServicePrincipal("apigateway.amazonaws.com"),
245 | action="lambda:InvokeFunction",
246 | source_arn=http_api_arn
247 | )
248 |
249 | ##########################################################
250 | # Create AWS API Gateway permissions
251 | ##########################################################
252 |
253 |
254 | ##########################################################
255 | # Stack exports
256 | ##########################################################
257 |
258 | CfnOutput(
259 | self,
260 | id=f"api-gateway-dynamic-publish-id",
261 | value=apigateway_id,
262 | export_name="api-gateway-dynamic-publish-id"
263 |
264 | )
265 |
266 | CfnOutput(
267 | self,
268 | id="api-gateway-dynamic-publish-endpoint",
269 | value=apigateway_endpoint,
270 | export_name="api-gateway-dynamic-publish-endpoint"
271 | )
272 |
273 | CfnOutput(
274 | self,
275 | id="api-gateway-dynamic-publish-stagename",
276 | value=apigateway_stagename,
277 | export_name="api-gateway-dynamic-publish-stagename"
278 | )
279 |
280 | CfnOutput(
281 | self,
282 | id="api-gateway-dynamic-publish-url",
283 | value=f"{apigateway_endpoint}/{apigateway_stagename}",
284 | export_name="api-gateway-dynamic-publish-url"
285 | )
286 |
287 | CfnOutput(
288 | self,
289 | id="api-gateway-dynamic-publish-arn",
290 | value=http_api_arn,
291 | export_name="api-gateway-dynamic-publish-arn"
292 | )
293 |
294 | CfnOutput(
295 | self,
296 | id="api-gateway-dynamic-publish-documentation-name",
297 | value=api_documentation_bucket.bucket_name,
298 | export_name="api-gateway-dynamic-publish-documentation-name"
299 | )
300 |
301 | ##########################################################
302 | # Stack exports
303 | ##########################################################
304 |
305 |
306 | def read_cdk_context_json(self):
307 | filename = "cdk.json"
308 |
309 | with open(filename, 'r') as my_file:
310 | data = my_file.read()
311 |
312 | obj = json.loads(data)
313 |
314 | return obj.get('context')
315 |
--------------------------------------------------------------------------------
/stacks/resources/api_creation/api_creator.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | api_creator.py:
5 | Cloudformation custom resource lambda handler which performs the following tasks:
6 | * injects lambda functions arns (created during CDK deployment) into the
7 | OpenAPI 3 spec file (api_definition.yaml)
8 | * deploys or updates the API Gateway stage using the OpenAPI 3 spec file (api_definition.yaml)
9 | * deletes the API Gateway stage (if the Cloudformation operation is delete)
10 | """
11 |
12 | import json
13 | import logging
14 | import os
15 |
16 | import boto3
17 | import yaml
18 |
19 | # set logging
20 | logger = logging.getLogger()
21 | logger.setLevel(logging.DEBUG)
22 |
23 | # environment variables
24 | aws_region = os.environ['AWS_REGION']
25 |
26 | # boto3 clients
27 | apigateway_client = boto3.client('apigatewayv2')
28 | s3_client = boto3.client('s3')
29 |
30 | def replace_placeholders(template_file: str, substitutions: dict) -> str:
31 | import re
32 |
33 | def from_dict(dct):
34 | def lookup(match):
35 | key = match.group(1)
36 | return dct.get(key, f'<{key} not found>')
37 | return lookup
38 |
39 | with open (template_file, "r") as template_file:
40 | template_data = template_file.read()
41 |
42 | # perform the subsitutions, looking for placeholders @@PLACEHOLDER@@
43 | api_template = re.sub('@@(.*?)@@', from_dict(substitutions), template_data)
44 |
45 | return api_template
46 |
47 |
48 | def get_api_by_name(api_name: str) -> str:
49 | get_apis = apigateway_client.get_apis()
50 | for api in get_apis['Items']:
51 | if api['Name'] == api_name:
52 | return api['ApiId']
53 |
54 | return None
55 |
56 |
57 | def create_api(api_template: str) -> str:
58 | api_response = apigateway_client.import_api(
59 | Body=api_template,
60 | FailOnWarnings=True
61 | )
62 |
63 | return api_response['ApiEndpoint'], api_response['ApiId']
64 |
65 |
66 | def update_api(api_template: str, api_name: str) -> str:
67 |
68 | api_id = get_api_by_name(api_name)
69 |
70 | if api_id is not None:
71 | api_response = apigateway_client.reimport_api(
72 | ApiId=api_id,
73 | Body=api_template,
74 | FailOnWarnings=True
75 | )
76 | return api_response['ApiEndpoint'], api_response['ApiId']
77 |
78 |
79 | def delete_api(api_name: str) -> None:
80 | if get_api_by_name(api_name) is not None:
81 | apigateway_client.delete_api(
82 | ApiId=get_api_by_name(api_name)
83 | )
84 |
85 |
86 | def deploy_api(
87 | api_id: str,
88 | api_stage_name: str,
89 | api_access_logs_arn: str,
90 | throttling_burst_limit: int,
91 | throttling_rate_limit: int
92 | ) -> None:
93 | apigateway_client.create_stage(
94 | AccessLogSettings={
95 | 'DestinationArn': api_access_logs_arn,
96 | 'Format': '$context.identity.sourceIp - - [$context.requestTime] "$context.httpMethod $context.routeKey $context.protocol" $context.status $context.responseLength $context.requestId $context.integrationErrorMessage'
97 | },
98 | ApiId=api_id,
99 | StageName=api_stage_name,
100 | AutoDeploy=True,
101 | DefaultRouteSettings={
102 | 'DetailedMetricsEnabled': True,
103 | 'ThrottlingBurstLimit':throttling_burst_limit,
104 | 'ThrottlingRateLimit': throttling_rate_limit
105 | }
106 | )
107 |
108 |
109 | def delete_api_deployment(api_id: str, api_stage_name: str) -> None:
110 | try:
111 | apigateway_client.get_stage(
112 | ApiId=api_id,
113 | StageName=api_stage_name
114 | )
115 |
116 | apigateway_client.delete_stage(
117 | ApiId=api_id,
118 | StageName=api_stage_name
119 | )
120 | except apigateway_client.exceptions.NotFoundException as e:
121 | logger.error(f"Stage name: {api_stage_name} for api id: {api_id} was not found during stage deletion. This is an expected error condition and is handled in code.")
122 | except Exception as e:
123 | raise ValueError(f"Unexpected error encountered during api deployment deletion: {str(e)}")
124 |
125 |
126 | def publish_api_documentation(bucket_name: str, api_definition: str) -> None:
127 |
128 | api_definition_json=json.dumps(yaml.safe_load(api_definition))
129 |
130 | with open("/tmp/swagger.json", "w") as swagger_file:
131 | swagger_file.write(api_definition_json)
132 |
133 | # Upload the file
134 | try:
135 |
136 | s3_client.upload_file("/tmp/swagger.json", bucket_name, "swagger.json")
137 |
138 | except Exception as e:
139 | logging.error(str(e))
140 | raise ValueError(str(e))
141 |
142 |
143 | def lambda_handler(event, context):
144 |
145 | # print the event details
146 | logger.debug(json.dumps(event, indent=2))
147 |
148 | props = event['ResourceProperties']
149 | api_gateway_access_log_group_arn = props['ApiGatewayAccessLogsLogGroupArn']
150 | api_integration_ping_lambda = props['ApiIntegrationPingLambda']
151 | api_integration_greetings_lambda = props['ApiIntegrationGreetingLambda']
152 | api_name = props['ApiName']
153 | api_stage_name = props['ApiStageName']
154 | api_documentation_bucket_name = props['ApiDocumentationBucketName']
155 | throttling_burst_limit = int(props['ThrottlingBurstLimit'])
156 | throttling_rate_limit = int(props['ThrottlingRateLimit'])
157 |
158 | lambda_substitutions = {
159 | "API_NAME": api_name,
160 | "API_INTEGRATION_PING_LAMBDA": f"arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{api_integration_ping_lambda}/invocations",
161 | "API_INTEGRATION_GREETING_LAMBDA": f"arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{api_integration_greetings_lambda}/invocations"
162 | }
163 |
164 | api_template = replace_placeholders("api_definition.yaml", lambda_substitutions)
165 |
166 | if event['RequestType'] != 'Delete':
167 |
168 | if get_api_by_name(api_name) is None:
169 |
170 | logger.debug("Creating API")
171 |
172 | api_endpoint, api_id = create_api(api_template)
173 |
174 | deploy_api(api_id, api_stage_name, api_gateway_access_log_group_arn, throttling_burst_limit, throttling_rate_limit)
175 |
176 | publish_api_documentation(api_documentation_bucket_name, api_template)
177 |
178 | output = {
179 | 'PhysicalResourceId': f"generated-api",
180 | 'Data': {
181 | 'ApiEndpoint': api_endpoint,
182 | 'ApiId': api_id,
183 | 'ApiStageName': api_stage_name
184 | }
185 | }
186 |
187 | return output
188 |
189 | else:
190 |
191 | logger.debug("Updating API")
192 |
193 | api_endpoint, api_id = update_api(api_template, api_name)
194 |
195 | # delete and redeploy the stage after updating the api definition
196 | delete_api_deployment(api_id, api_stage_name)
197 | deploy_api(api_id, api_stage_name, api_gateway_access_log_group_arn, throttling_burst_limit, throttling_rate_limit)
198 |
199 | publish_api_documentation(api_documentation_bucket_name, api_template)
200 |
201 | output = {
202 | 'PhysicalResourceId': f"generated-api",
203 | 'Data': {
204 | 'ApiEndpoint': api_endpoint,
205 | 'ApiId': api_id,
206 | 'ApiStageName': api_stage_name
207 | }
208 | }
209 |
210 | return output
211 |
212 | if event['RequestType'] == 'Delete':
213 |
214 | logger.debug("Deleting API")
215 |
216 | if get_api_by_name(api_name) is not None:
217 | delete_api(api_name)
218 |
219 | output = {
220 | 'PhysicalResourceId': f"generated-api",
221 | 'Data': {
222 | 'ApiEndpoint': "Deleted",
223 | 'ApiId': "Deleted",
224 | 'ApiStageName': "Deleted"
225 | }
226 | }
227 | logger.info(output)
228 |
229 | return output
230 |
--------------------------------------------------------------------------------
/stacks/resources/api_creation/api_definition.yaml:
--------------------------------------------------------------------------------
1 | openapi: "3.0.0"
2 | info:
3 | title: @@API_NAME@@
4 | version: "v1.0"
5 | x-amazon-apigateway-request-validators:
6 | all:
7 | validateRequestBody: true
8 | validateRequestParameters: true
9 | params-only:
10 | validateRequestBody: false
11 | validateRequestParameters: true
12 | x-amazon-apigateway-request-validator: all
13 | paths:
14 | /ping:
15 | get:
16 | summary: "Simulates an API Ping"
17 | description: |
18 | ## Simulates an API Ping
19 |
20 | The purpose of this endpoint is to simulate a Ping request and respond with a Pong answer.
21 |
22 | ### Sample invocation
23 |
24 | ```bash
25 | #!/bin/bash
26 |
27 | # set the desired AWS region below
28 | AWS_REGION="us-east-1"
29 |
30 | STACK_NAME="ApiGatewayDynamicPublish"
31 |
32 | # Get the API Endpoint
33 | API_ENDPOINT_URL_EXPORT_NAME="api-gateway-dynamic-publish-url"
34 | API_ENDPOINT_URL=$(aws cloudformation --region ${AWS_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${API_ENDPOINT_URL_EXPORT_NAME}'].OutputValue" --output text)
35 | API_GATEWAY_URL="${API_ENDPOINT_URL}"
36 |
37 | ################################################
38 | # TEST Ping API
39 | ################################################
40 |
41 | echo "Testing GET ${API_GATEWAY_URL}/ping"
42 | API_RESPONSE=$(curl -sX GET ${API_GATEWAY_URL}/ping)
43 |
44 | echo ""
45 | echo ${API_RESPONSE}
46 | echo ""
47 | ```
48 |
49 | ### Sample response
50 |
51 | ```json
52 | {
53 | "ping": "Pong"
54 | }
55 | ```
56 | operationId: "pingIntegration"
57 | x-amazon-apigateway-request-validator: all
58 | responses:
59 | 200:
60 | description: "OK"
61 | content:
62 | application/json:
63 | schema:
64 | type: array
65 | items:
66 | $ref: "#/components/schemas/PingResponse"
67 | 500:
68 | description: "Internal Server Error"
69 | x-amazon-apigateway-integration:
70 | uri: @@API_INTEGRATION_PING_LAMBDA@@
71 | payloadFormatVersion: "2.0"
72 | httpMethod: "POST"
73 | type: "aws_proxy"
74 | connectionType: "INTERNET"
75 | /greeting:
76 | get:
77 | summary: "Get a greeting message"
78 | description: |
79 | ## Get a greeting message
80 |
81 | The purpose of this endpoint is send a greeting string and receive a greeting message.
82 |
83 | ### Sample invocation
84 |
85 | ```bash
86 | #!/bin/bash
87 |
88 | # set the desired AWS region below
89 | AWS_REGION="us-east-1"
90 |
91 | # set the greeting you would like to send, ensure it is a single word with no spaces
92 | GREETING="world"
93 |
94 | STACK_NAME="ApiGatewayDynamicPublish"
95 |
96 | # Get the API Endpoint
97 | API_ENDPOINT_URL_EXPORT_NAME="api-gateway-dynamic-publish-url"
98 | API_ENDPOINT_URL=$(aws cloudformation --region ${AWS_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${API_ENDPOINT_URL_EXPORT_NAME}'].OutputValue" --output text)
99 | API_GATEWAY_URL="${API_ENDPOINT_URL}"
100 |
101 | ################################################
102 | # TEST Greetings API
103 | ################################################
104 |
105 | echo "Testing GET ${API_GATEWAY_URL}/greetings?greeting=${GREETING}"
106 | API_RESPONSE=$(curl -sX GET ${API_GATEWAY_URL}/greetings?greeting=${GREETING})
107 |
108 | echo ""
109 | echo ${API_RESPONSE} | jq .
110 | echo ""
111 | ```
112 |
113 | ### Sample response
114 |
115 | ```json
116 | {
117 | "greeting": "Hello World"
118 | }
119 | ```
120 | operationId: "greetingIntegration"
121 | x-amazon-apigateway-request-validator: all
122 | parameters:
123 | - in: query
124 | name: greeting
125 | schema:
126 | type: string
127 | description: |
128 | A greeting string which the API will combine to form a greeting message
129 | responses:
130 | 200:
131 | description: "OK"
132 | content:
133 | application/json:
134 | schema:
135 | type: array
136 | items:
137 | $ref: "#/components/schemas/GreetingResponse"
138 | 500:
139 | description: "Internal Server Error"
140 | x-amazon-apigateway-integration:
141 | uri: @@API_INTEGRATION_GREETING_LAMBDA@@
142 | payloadFormatVersion: "2.0"
143 | httpMethod: "POST"
144 | type: "aws_proxy"
145 | connectionType: "INTERNET"
146 | components:
147 | schemas:
148 | PingResponse:
149 | type: object
150 | properties:
151 | ping:
152 | type: string
153 | description: |
154 | Response to the ping request.
155 | GreetingResponse:
156 | type: object
157 | properties:
158 | greeting:
159 | type: string
160 | description: |
161 | The greeting response which concatenates the incoming greeting to form a greeting message.
--------------------------------------------------------------------------------
/stacks/resources/api_creation/requirements.txt:
--------------------------------------------------------------------------------
1 | PyYAML==6.0
2 |
--------------------------------------------------------------------------------
/stacks/resources/api_integrations/greeting.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | greeting.py:
5 | Simple Lambda Function handler that is the target for
6 | the API Gateway "Greeting" endpoint.
7 | """
8 |
9 | import json
10 | import logging
11 | import traceback
12 |
13 | # set logging
14 | logger = logging.getLogger()
15 | logger.setLevel(logging.DEBUG)
16 |
17 | def lambda_handler(event, context):
18 | # read the event to json
19 | logger.debug(json.dumps(event, indent=2))
20 |
21 | try:
22 |
23 | # verify that the api_key query parameter is provided
24 | if 'queryStringParameters' not in event or 'greeting' not in event['queryStringParameters']:
25 | raise ValueError(f"greeting is expected as a query parameter but it was not present in the request; {event['rawPath']}")
26 |
27 | message = {"greeting": f"Hello {event['queryStringParameters']['greeting']}"}
28 |
29 | return {
30 | 'statusCode': 200,
31 | 'body': json.dumps(message, indent=2),
32 | 'headers': {'Content-Type': 'application/json'}
33 | }
34 |
35 | except Exception as e:
36 |
37 | traceback.print_exception(type(e), value=e, tb=e.__traceback__)
38 |
39 | logger.error(f'Greeting error: {str(e)}')
40 |
41 | api_error = {"error": str(e)}
42 |
43 | return {
44 | 'statusCode': 500,
45 | 'body': json.dumps(api_error),
46 | 'headers': {'Content-Type': 'application/json'}
47 | }
48 |
--------------------------------------------------------------------------------
/stacks/resources/api_integrations/ping.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | ping.py:
5 | Simple Lambda Function handler that is the target for
6 | the API Gateway "Ping" endpoint.
7 | """
8 |
9 | import json
10 | import logging
11 | import traceback
12 |
13 | # set logging
14 | logger = logging.getLogger()
15 | logger.setLevel(logging.DEBUG)
16 |
17 | def lambda_handler(event, context):
18 | # read the event to json
19 | logger.debug(json.dumps(event, indent=2))
20 |
21 | try:
22 |
23 | message = {"ping": "Pong"}
24 |
25 | return {
26 | 'statusCode': 200,
27 | 'body': json.dumps(message, indent=2),
28 | 'headers': {'Content-Type': 'application/json'}
29 | }
30 |
31 | except Exception as e:
32 |
33 | traceback.print_exception(type(e), value=e, tb=e.__traceback__)
34 |
35 | logger.error(f'Ping error: {str(e)}')
36 |
37 | api_error = {"error": str(e)}
38 |
39 | return {
40 | 'statusCode': 500,
41 | 'body': json.dumps(api_error),
42 | 'headers': {'Content-Type': 'application/json'}
43 | }
44 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_apigateway_dynamic_publish.py:
--------------------------------------------------------------------------------
1 | import aws_cdk as cdk
2 | from aws_cdk.assertions import Template
3 | from aws_cdk.assertions import Match
4 |
5 | from stacks.apigateway_dynamic_publish import ApiGatewayDynamicPublishStack
6 |
7 |
8 | def test_synthesizes_properly():
9 | app = cdk.App()
10 |
11 | # Create the ProcessorStack.
12 | apigateway_dynamic_publish_stack = ApiGatewayDynamicPublishStack(
13 | app,
14 | "ApiGatewayDynamicPublishStack"
15 | )
16 |
17 | # Prepare the stack for assertions.
18 | template = Template.from_stack(apigateway_dynamic_publish_stack)
19 |
20 | # Assert that we have the expected resources
21 | template.resource_count_is("AWS::S3::Bucket", 1)
22 | template.resource_count_is("AWS::KMS::Key", 1)
23 | template.resource_count_is("AWS::Lambda::Function", 5)
24 | template.resource_count_is("AWS::IAM::Role", 4)
25 | template.resource_count_is("AWS::IAM::Policy", 3)
26 | template.resource_count_is("AWS::Lambda::Permission", 2)
27 | template.resource_count_is("AWS::CloudFormation::CustomResource", 1)
28 | template.resource_count_is("Custom::S3AutoDeleteObjects", 1)
29 | template.resource_count_is("AWS::S3::BucketPolicy", 1)
30 |
31 | template.has_resource_properties(
32 | "AWS::IAM::Role",
33 | Match.object_equals(
34 | {
35 | "AssumeRolePolicyDocument": {
36 | "Statement": [
37 | {
38 | "Action": "sts:AssumeRole",
39 | "Effect": "Allow",
40 | "Principal": {
41 | "Service": "lambda.amazonaws.com"
42 | }
43 | }
44 | ],
45 | "Version": "2012-10-17"
46 | },
47 | "ManagedPolicyArns": [
48 | {
49 | "Fn::Join": [
50 | "",
51 | [
52 | "arn:",
53 | {
54 | "Ref": "AWS::Partition"
55 | },
56 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
57 | ]
58 | ]
59 | }
60 | ]
61 | }
62 | )
63 | )
64 |
65 | template.has_resource_properties(
66 | "AWS::IAM::Policy",
67 | Match.object_equals(
68 | {
69 | "PolicyDocument": {
70 | "Statement": [
71 | {
72 | "Action": [
73 | "apigateway:DELETE",
74 | "apigateway:PUT",
75 | "apigateway:PATCH",
76 | "apigateway:POST",
77 | "apigateway:GET"
78 | ],
79 | "Effect": "Allow",
80 | "Resource": [
81 | "arn:aws:apigateway:*::/apis/*",
82 | "arn:aws:apigateway:*::/apis"
83 | ]
84 | },
85 | {
86 | "Action": "logs:*",
87 | "Effect": "Allow",
88 | "Resource": "*"
89 | },
90 | {
91 | "Action": [
92 | "s3:GetObject*",
93 | "s3:GetBucket*",
94 | "s3:List*",
95 | "s3:DeleteObject*",
96 | "s3:PutObject",
97 | "s3:PutObjectLegalHold",
98 | "s3:PutObjectRetention",
99 | "s3:PutObjectTagging",
100 | "s3:PutObjectVersionTagging",
101 | "s3:Abort*"
102 | ],
103 | "Effect": "Allow",
104 | "Resource": [
105 | {
106 | "Fn::GetAtt": [
107 | Match.any_value(),
108 | "Arn"
109 | ]
110 | },
111 | {
112 | "Fn::Join": [
113 | "",
114 | [
115 | {
116 | "Fn::GetAtt": [
117 | Match.any_value(),
118 | "Arn"
119 | ]
120 | },
121 | "/*"
122 | ]
123 | ]
124 | }
125 | ]
126 | },
127 | {
128 | "Action": [
129 | "kms:Decrypt",
130 | "kms:DescribeKey",
131 | "kms:Encrypt",
132 | "kms:ReEncrypt*",
133 | "kms:GenerateDataKey*"
134 | ],
135 | "Effect": "Allow",
136 | "Resource": {
137 | "Fn::GetAtt": [
138 | Match.any_value(),
139 | "Arn"
140 | ]
141 | }
142 | }
143 | ],
144 | "Version": "2012-10-17"
145 | },
146 | "PolicyName": Match.any_value(),
147 | "Roles": [
148 | {
149 | "Ref": Match.any_value()
150 | }
151 | ]
152 | }
153 | )
154 | )
155 |
156 | template.has_resource_properties(
157 | "AWS::Lambda::Permission",
158 | Match.object_equals(
159 | {
160 | "Action": "lambda:InvokeFunction",
161 | "FunctionName": {
162 | "Fn::GetAtt": [
163 | Match.any_value(),
164 | "Arn"
165 | ]
166 | },
167 | "Principal": "apigateway.amazonaws.com",
168 | "SourceArn": {
169 | "Fn::Join": [
170 | "",
171 | [
172 | "arn:",
173 | {
174 | "Ref": "AWS::Partition"
175 | },
176 | ":execute-api:",
177 | {
178 | "Ref": "AWS::Region"
179 | },
180 | ":",
181 | {
182 | "Ref": "AWS::AccountId"
183 | },
184 | ":",
185 | {
186 | "Fn::GetAtt": [
187 | "ApiCreatorCustomResource",
188 | "ApiId"
189 | ]
190 | },
191 | "/*/*/*"
192 | ]
193 | ]
194 | }
195 | }
196 | )
197 | )
198 |
199 | template.has_resource_properties(
200 | "AWS::CloudFormation::CustomResource",
201 | Match.object_equals(
202 | {
203 | "ServiceToken": {
204 | "Fn::GetAtt": [
205 | Match.any_value(),
206 | "Arn"
207 | ]
208 | },
209 | "ApiGatewayAccessLogsLogGroupArn": {
210 | "Fn::GetAtt": [
211 | Match.any_value(),
212 | "Arn"
213 | ]
214 | },
215 | "ApiIntegrationPingLambda": {
216 | "Fn::GetAtt": [
217 | Match.any_value(),
218 | "Arn"
219 | ]
220 | },
221 | "ApiIntegrationGreetingLambda": {
222 | "Fn::GetAtt": [
223 | Match.any_value(),
224 | "Arn"
225 | ]
226 | },
227 | "ApiDocumentationBucketName": {
228 | "Ref": Match.any_value()
229 | },
230 | "ApiDocumentationBucketUrl": {
231 | "Fn::GetAtt": [
232 | Match.any_value(),
233 | "WebsiteURL"
234 | ]
235 | },
236 | "ApiName": Match.any_value(),
237 | "ApiStageName": Match.any_value(),
238 | "ThrottlingBurstLimit": Match.any_value(),
239 | "ThrottlingRateLimit": Match.any_value()
240 | }
241 | )
242 | )
243 |
244 | template.has_resource_properties(
245 | "Custom::S3AutoDeleteObjects",
246 | Match.object_equals(
247 | {
248 | "ServiceToken": {
249 | "Fn::GetAtt": [
250 | Match.any_value(),
251 | "Arn"
252 | ]
253 | },
254 | "BucketName": {
255 | "Ref": Match.any_value()
256 | }
257 | }
258 | )
259 | )
260 |
261 | template.has_resource_properties(
262 | "AWS::S3::BucketPolicy",
263 | Match.object_equals(
264 | {
265 | "Bucket": {
266 | "Ref": Match.any_value()
267 | },
268 | "PolicyDocument": {
269 | "Statement": [
270 | {
271 | "Action": [
272 | "s3:GetBucket*",
273 | "s3:List*",
274 | "s3:DeleteObject*"
275 | ],
276 | "Effect": "Allow",
277 | "Principal": {
278 | "AWS": {
279 | "Fn::GetAtt": [
280 | Match.any_value(),
281 | "Arn"
282 | ]
283 | }
284 | },
285 | "Resource": [
286 | {
287 | "Fn::GetAtt": [
288 | Match.any_value(),
289 | "Arn"
290 | ]
291 | },
292 | {
293 | "Fn::Join": [
294 | "",
295 | [
296 | {
297 | "Fn::GetAtt": [
298 | Match.any_value(),
299 | "Arn"
300 | ]
301 | },
302 | "/*"
303 | ]
304 | ]
305 | }
306 | ]
307 | }
308 | ],
309 | "Version": "2012-10-17"
310 | }
311 | }
312 | )
313 | )
314 |
315 | template.has_resource_properties(
316 | "Custom::S3AutoDeleteObjects",
317 | Match.object_equals(
318 | {
319 | "ServiceToken": {
320 | "Fn::GetAtt": [
321 | Match.any_value(),
322 | "Arn"
323 | ]
324 | },
325 | "BucketName": {
326 | "Ref": Match.any_value()
327 | }
328 | }
329 | )
330 | )
331 |
332 | template.has_resource_properties(
333 | "AWS::S3::Bucket",
334 | Match.object_equals(
335 | {
336 | "BucketEncryption": {
337 | "ServerSideEncryptionConfiguration": [
338 | {
339 | "ServerSideEncryptionByDefault": {
340 | "KMSMasterKeyID": {
341 | "Fn::GetAtt": [
342 | Match.any_value(),
343 | "Arn"
344 | ]
345 | },
346 | "SSEAlgorithm": "aws:kms"
347 | }
348 | }
349 | ]
350 | },
351 | "PublicAccessBlockConfiguration": {
352 | "BlockPublicAcls": True,
353 | "BlockPublicPolicy": True,
354 | "IgnorePublicAcls": True,
355 | "RestrictPublicBuckets": True
356 | },
357 | "Tags": [
358 | {
359 | "Key": "aws-cdk:auto-delete-objects",
360 | "Value": "true"
361 | }
362 | ]
363 | }
364 | )
365 | )
366 |
367 | template.has_resource_properties(
368 | "AWS::KMS::Key",
369 | Match.object_equals(
370 | {
371 | "KeyPolicy": {
372 | "Statement": [
373 | {
374 | "Action": "kms:*",
375 | "Effect": "Allow",
376 | "Principal": {
377 | "AWS": {
378 | "Fn::Join": [
379 | "",
380 | [
381 | "arn:",
382 | {
383 | "Ref": "AWS::Partition"
384 | },
385 | ":iam::",
386 | {
387 | "Ref": "AWS::AccountId"
388 | },
389 | ":root"
390 | ]
391 | ]
392 | }
393 | },
394 | "Resource": "*"
395 | }
396 | ],
397 | "Version": "2012-10-17"
398 | },
399 | "Description": Match.any_value()
400 | }
401 | )
402 | )
403 |
--------------------------------------------------------------------------------
/view_api_docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ###################################################################
4 | # Script Name : view_api_docs.sh
5 | # Description : Downloads Swagger UI, incorporates the
6 | # api-gateway-dynamic-project OpenAPI v3 spec
7 | # file into Swagger UI and runs Swagger UI
8 | # via a local node.js server which, by default,
9 | # listens on http://localhost:12345
10 | # Args :
11 | # Author : Damian McDonald
12 | ###################################################################
13 |
14 | ### check if AWS credential variables are correctly set
15 | if [ -z "${AWS_ACCESS_KEY_ID}" ]
16 | then
17 | echo "AWS credential variable AWS_ACCESS_KEY_ID is empty."
18 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
19 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
20 | fi
21 |
22 | if [ -z "${AWS_SECRET_ACCESS_KEY}" ]
23 | then
24 | echo "AWS credential variable AWS_SECRET_ACCESS_KEY is empty."
25 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
26 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
27 | fi
28 |
29 | if [ -z "${AWS_DEFAULT_REGION}" ]
30 | then
31 | echo "AWS credential variable AWS_DEFAULT_REGION is empty."
32 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment."
33 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"
34 | fi
35 | ### check if AWS credential variables are correctly set
36 |
37 | # define the swagger ui version to download
38 | SWAGGER_UI_RELEASE=4.5.2
39 | SWAGGER_UI_RELEASE_URL=https://github.com/swagger-api/swagger-ui/archive/refs/tags/v${SWAGGER_UI_RELEASE}.zip
40 |
41 | # define the root directory
42 | ROOT_DIR=$PWD
43 |
44 | echo "Preparing documentation ..."
45 |
46 | # AWS Cloudformation stack name
47 | STACK_NAME="ApiGatewayDynamicPublish"
48 |
49 | echo "Grabbing Cloudformation exports for Stack Name: ${STACK_NAME}"
50 |
51 | # the s3 bucket name containing the api documentation
52 | DOC_BUCKET_EXPORT_NAME="api-gateway-dynamic-publish-documentation-name"
53 | BUCKET_NAME=$(aws cloudformation --region ${AWS_DEFAULT_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${DOC_BUCKET_EXPORT_NAME}'].OutputValue" --output text)
54 |
55 | # ask the user for permission to download swagger ui
56 | echo ""
57 | echo "##################################"
58 | echo ""
59 | echo "To view API documentation in the OpenAPI format, it is necessary to download Swagger UI."
60 | echo "Swagger UI is a third party, open source project licenced under the Apache License 2.0."
61 | echo "See https://github.com/swagger-api/swagger-ui"
62 | echo ""
63 | echo "##################################"
64 | echo ""
65 |
66 | while true; do
67 | read -p "Would you like to download Swagger UI to view the API documentation? [y/n]" yn
68 | case $yn in
69 | [Yy]* ) break;;
70 | [Nn]* ) exit;;
71 | * ) echo "Please answer y[yes] or n[no].";;
72 | esac
73 | done
74 |
75 | # create a tmp download directory
76 | mkdir -p ${ROOT_DIR}/apidocs/.tmp
77 |
78 | # create the swagger-ui directory
79 | mkdir -p ${ROOT_DIR}/apidocs/swagger-ui
80 |
81 | # grab the swagger ui release
82 | wget --quiet -O ${ROOT_DIR}/apidocs/.tmp/v${SWAGGER_UI_RELEASE}.zip ${SWAGGER_UI_RELEASE_URL}
83 | cd ${ROOT_DIR}/apidocs/.tmp
84 |
85 | # unzip the swagger ui release and copy the dist folder
86 | unzip -qq v${SWAGGER_UI_RELEASE}.zip
87 | cp -r ${ROOT_DIR}/apidocs/.tmp/swagger-ui-${SWAGGER_UI_RELEASE}/dist/ ${ROOT_DIR}/apidocs/swagger-ui
88 |
89 | # delete the tmp directory
90 | rm -fr ${ROOT_DIR}/apidocs/.tmp
91 |
92 | # grab the dynamic swagger.json file
93 | cd ${ROOT_DIR}/apidocs/swagger-ui
94 | aws s3 cp s3://${BUCKET_NAME}/swagger.json swagger.json --region ${AWS_DEFAULT_REGION}
95 |
96 | # replace the defeault json path with the swagger.json downloaded from s3
97 | sed 's,https://petstore.swagger.io/v2/swagger.json,swagger.json,g' index.html | tee index.html > /dev/null 2>&1
98 |
99 | cd ${ROOT_DIR}/apidocs
100 |
101 | # install the npm dependencies
102 | npm install express
103 |
104 | # run a local nodejs server to visualize the dynamic documentation
105 | node server.js
106 |
107 | # return to starting directory
108 | cd ${ROOT_DIR}
--------------------------------------------------------------------------------